From 23c4404b955fbc1a6904b7140f911d9f29de1f27 Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Tue, 29 Sep 2026 16:49:03 +0200 Subject: [PATCH 001/100] fix(codex): keep output schema fields named like schema keywords (#5684) The Codex output schema normalizer walked every key as if it held a schema, so a field named "properties" (or a literal default, enum, or const value shaped like an object schema) was rewritten with invented required and additionalProperties entries. Codex then rejected the turn with invalid_json_schema. Walk only the keywords that hold schemas. Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> --- .../providers/codex-app-server-agent.test.ts | 65 +++++++++++++++++++ .../agent/providers/codex-app-server-agent.ts | 56 ++++++++++++++-- 2 files changed, 117 insertions(+), 4 deletions(-) diff --git a/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts b/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts index 1d84dbca0c..2c74912a47 100644 --- a/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts +++ b/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts @@ -2373,6 +2373,71 @@ describe("Codex app-server provider", () => { }); }); + test("keeps an output field named properties as a field", () => { + const input = { + type: "object", + properties: { properties: { type: "string" } }, + required: ["properties"], + additionalProperties: false, + }; + + expect(normalizeCodexOutputSchema(input)).toEqual(input); + }); + + test("normalizes schemas under keywords and leaves literal values alone", () => { + const literal = { type: "object", properties: { type: "string" } }; + const input = { + type: "object", + properties: { + result: { $ref: "#/$defs/result" }, + choice: { anyOf: [{ type: "object", properties: { id: { type: "string" } } }] }, + }, + $defs: { + properties: { type: "string" }, + result: { + type: "object", + properties: { value: { type: "string" } }, + default: literal, + examples: [literal], + enum: [literal], + const: literal, + }, + }, + }; + + expect(normalizeCodexOutputSchema(input)).toEqual({ + type: "object", + properties: { + result: { $ref: "#/$defs/result" }, + choice: { + anyOf: [ + { + type: "object", + properties: { id: { type: "string" } }, + required: ["id"], + additionalProperties: false, + }, + ], + }, + }, + required: ["result", "choice"], + additionalProperties: false, + $defs: { + properties: { type: "string" }, + result: { + type: "object", + properties: { value: { type: "string" } }, + default: literal, + examples: [literal], + enum: [literal], + const: literal, + required: ["value"], + additionalProperties: false, + }, + }, + }); + }); + test("passes a normalized output schema to turn/start", async () => { const session = createSession(); const request = vi.fn(async (method: string) => { diff --git a/packages/server/src/server/agent/providers/codex-app-server-agent.ts b/packages/server/src/server/agent/providers/codex-app-server-agent.ts index 1d0aca552c..f16c5fb6d8 100644 --- a/packages/server/src/server/agent/providers/codex-app-server-agent.ts +++ b/packages/server/src/server/agent/providers/codex-app-server-agent.ts @@ -365,19 +365,67 @@ function isObjectSchemaNode(schema: Record): boolean { ); } -function normalizeCodexOutputSchemaNode(schema: unknown, schemaPath: string): unknown { +// Keywords whose value maps names to schemas. +const SCHEMA_MAP_KEYWORDS = [ + "properties", + "patternProperties", + "$defs", + "definitions", + "dependentSchemas", +] as const; + +// Keywords whose value is a schema or an array of schemas. +const SUBSCHEMA_KEYWORDS = [ + "items", + "prefixItems", + "additionalItems", + "contains", + "additionalProperties", + "unevaluatedProperties", + "unevaluatedItems", + "propertyNames", + "allOf", + "anyOf", + "oneOf", + "not", + "if", + "then", + "else", +] as const; + +function normalizeCodexOutputSubschema(schema: unknown, schemaPath: string): unknown { if (Array.isArray(schema)) { return schema.map((entry, index) => normalizeCodexOutputSchemaNode(entry, `${schemaPath}[${index}]`), ); } + return normalizeCodexOutputSchemaNode(schema, schemaPath); +} + +function normalizeCodexOutputSchemaNode(schema: unknown, schemaPath: string): unknown { if (!isSchemaRecord(schema)) { return schema; } - const normalized: Record = {}; - for (const [key, value] of Object.entries(schema)) { - normalized[key] = normalizeCodexOutputSchemaNode(value, `${schemaPath}.${key}`); + const normalized: Record = { ...schema }; + for (const keyword of SCHEMA_MAP_KEYWORDS) { + const schemas = schema[keyword]; + if (isSchemaRecord(schemas)) { + normalized[keyword] = Object.fromEntries( + Object.entries(schemas).map(([name, child]) => [ + name, + normalizeCodexOutputSchemaNode(child, `${schemaPath}.${keyword}.${name}`), + ]), + ); + } + } + for (const keyword of SUBSCHEMA_KEYWORDS) { + if (keyword in schema) { + normalized[keyword] = normalizeCodexOutputSubschema( + schema[keyword], + `${schemaPath}.${keyword}`, + ); + } } if (!isObjectSchemaNode(normalized)) { From a8f5d969b82c6c06ecdc00e7fef4a044bae71960 Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Tue, 29 Sep 2026 17:53:43 +0200 Subject: [PATCH 002/100] fix(codex): keep the default effort after a daemon restart (#5686) A Codex agent created without a thinking option ran on the configured default effort until the daemon restarted. The resumed session never resolved the defaults, so it showed thinking auto and sent turns with no effort. Resolve unset model and effort for a resumed thread the same way a new thread does. Closes #5673 Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> --- .../providers/codex-app-server-agent.test.ts | 30 +++++++++++++++++++ .../agent/providers/codex-app-server-agent.ts | 14 +++++++-- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts b/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts index 2c74912a47..d55a0b2a79 100644 --- a/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts +++ b/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts @@ -1170,6 +1170,36 @@ describe("Codex app-server provider", () => { } }); + test("keeps the default effort when a session without one is resumed after a restart", async () => { + const beforeRestart = createFakeCodexAppServer(); + const firstSession = await createProviderWithFakeAppServer(beforeRestart).createSession( + createConfig({ thinkingOptionId: undefined }), + ); + await firstSession.startTurn("first turn"); + await expect(beforeRestart.waitForTurnStart()).resolves.toMatchObject({ effort: "medium" }); + const handle = firstSession.describePersistence()!; + await firstSession.close(); + + const afterRestart = createFakeCodexAppServer(); + // The daemon resumes with the stored agent config, which never recorded an effort. + const resumed = await createProviderWithFakeAppServer(afterRestart).resumeSession(handle, { + model: "gpt-5.4", + thinkingOptionId: undefined, + }); + + try { + await expect(resumed.getRuntimeInfo()).resolves.toMatchObject({ + thinkingOptionId: "medium", + }); + await resumed.startTurn("turn after restart"); + await expect(afterRestart.waitForTurnStart()).resolves.toMatchObject({ effort: "medium" }); + beforeRestart.assertNoErrors(); + afterRestart.assertNoErrors(); + } finally { + await resumed.close(); + } + }); + test("preapproves only granted tools on the injected Codex MCP server", async () => { const session = createSession({ modeId: undefined, diff --git a/packages/server/src/server/agent/providers/codex-app-server-agent.ts b/packages/server/src/server/agent/providers/codex-app-server-agent.ts index f16c5fb6d8..f86d5de653 100644 --- a/packages/server/src/server/agent/providers/codex-app-server-agent.ts +++ b/packages/server/src/server/agent/providers/codex-app-server-agent.ts @@ -3611,6 +3611,7 @@ export class CodexAppServerAgentSession implements AgentSession { if (this.currentThreadId) { await this.ensureThreadLoaded(); await this.loadPersistedHistory(this.client); + await this.applyDefaultModelAndThinking(); } if (this.closed) { @@ -5208,13 +5209,20 @@ export class CodexAppServerAgentSession implements AgentSession { return { model, thinkingOptionId }; } + // A session without a model or effort runs on Codex's configured defaults, whether its + // thread is new or resumed after a restart. + private async applyDefaultModelAndThinking(): Promise { + const { model, thinkingOptionId } = await this.resolveModelAndThinking(); + this.config.model = model; + this.config.thinkingOptionId = thinkingOptionId; + return model; + } + private async ensureThread(): Promise { if (!this.client) return; if (this.currentThreadId) return; - const { model, thinkingOptionId } = await this.resolveModelAndThinking(); - this.config.model = model; - this.config.thinkingOptionId = thinkingOptionId; + const model = await this.applyDefaultModelAndThinking(); const { params, approvalPolicy, sandbox } = this.buildThreadStartRequest(model); const rawResponse = await this.client.request("thread/start", params); From 4b45bc9fc10630dc49ef00796f82d467aec84786 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Tue, 29 Sep 2026 17:25:21 +0100 Subject: [PATCH 003/100] fix: restore archived workspaces without recorded branches (#5694) Create a fresh branch from the recorded base when available, or the repository default, so archived agents remain accessible. Cover the server recovery choices and the browser restore-to-agent flow. --- .../app/e2e/browser/worktree-restore.spec.ts | 91 ++++++++++++++++++- packages/app/src/i18n/resources/ar.ts | 4 +- packages/app/src/i18n/resources/en.ts | 4 +- packages/app/src/i18n/resources/es.ts | 4 +- packages/app/src/i18n/resources/fr.ts | 4 +- packages/app/src/i18n/resources/ja.ts | 4 +- packages/app/src/i18n/resources/ko.ts | 4 +- packages/app/src/i18n/resources/pt-BR.ts | 4 +- packages/app/src/i18n/resources/ru.ts | 4 +- packages/app/src/i18n/resources/zh-CN.ts | 4 +- .../workspace/workspace-route-state-views.tsx | 24 +++-- .../resolve-worktree-creation-intent.ts | 5 +- .../workspace-recovery-service.test.ts | 61 +++++++++++++ .../workspace-recovery-service.ts | 48 ++++++---- packages/server/src/utils/worktree.ts | 18 ++++ 15 files changed, 246 insertions(+), 37 deletions(-) diff --git a/packages/app/e2e/browser/worktree-restore.spec.ts b/packages/app/e2e/browser/worktree-restore.spec.ts index 479a69ffb8..6158906f3f 100644 --- a/packages/app/e2e/browser/worktree-restore.spec.ts +++ b/packages/app/e2e/browser/worktree-restore.spec.ts @@ -1,6 +1,7 @@ import { randomUUID } from "node:crypto"; import { execFileSync } from "node:child_process"; -import { existsSync, writeFileSync } from "node:fs"; +import { existsSync, mkdirSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; import path from "node:path"; import { restoreArchivedWorkspace, @@ -354,6 +355,88 @@ test.describe("Worktree restore", () => { .toBeNull(); }); + test("restores a branchless workspace so its archived agent can be used again", async ({ + page, + }) => { + const project = await openProjectViaDaemon(worktreeClient, tempRepo.path); + createdProjectIds.add(project.projectKey); + const backingWorktree = await createWorktreeViaDaemon(worktreeClient, { + cwd: tempRepo.path, + slug: `branchless-${randomUUID().slice(0, 8)}`, + }); + createdProjectIds.add(backingWorktree.projectKey); + createdWorktreeDirectories.add(backingWorktree.workspaceDirectory); + + execFileSync("git", ["switch", "--detach", "main"], { + cwd: backingWorktree.workspaceDirectory, + stdio: "pipe", + }); + const created = await worktreeClient.createWorkspace({ + source: { kind: "directory", path: backingWorktree.workspaceDirectory }, + title: "Sample workspace", + }); + if (!created.workspace) { + throw new Error(created.error ?? "Could not create the branchless workspace"); + } + const workspaceId = created.workspace.id; + const agent = await createIdleAgent(client, { + cwd: backingWorktree.workspaceDirectory, + workspaceId, + title: "Archived agent", + }); + + await archiveWorkspaceFromDaemon(worktreeClient, backingWorktree.workspaceDirectory, { + scope: "worktree", + }); + await expect + .poll(() => existsSync(backingWorktree.workspaceDirectory), { timeout: 30_000 }) + .toBe(false); + await expect.poll(() => fetchAgentArchivedAt(client, agent.id)).not.toBeNull(); + + await gotoAppShell(page); + await waitForSidebarHydration(page); + await openSessions(page); + await page.getByTestId(`agent-row-${getServerId()}-${agent.id}`).click(); + await expect(page.getByText("Workspace archived", { exact: true })).toBeVisible(); + await expect( + page.getByText( + "Restore Sample workspace to return to its agents. A new branch will start from the saved base or the repository default.", + { exact: true }, + ), + ).toBeVisible(); + await captureBranchlessRestoreStep(page, "01-restore-workspace.png"); + + await page.getByRole("button", { name: "Restore", exact: true }).click(); + await expect + .poll(() => existsSync(backingWorktree.workspaceDirectory), { timeout: 30_000 }) + .toBe(true); + await expect(page.getByRole("button", { name: "Unarchive", exact: true })).toBeVisible({ + timeout: 30_000, + }); + await captureBranchlessRestoreStep(page, "02-unarchive-agent.png"); + + await page.getByRole("button", { name: "Unarchive", exact: true }).click(); + await expect.poll(() => fetchAgentArchivedAt(client, agent.id)).toBeNull(); + await expect(page.getByRole("textbox", { name: "Message agent..." })).toBeEditable(); + await captureBranchlessRestoreStep(page, "03-agent-ready.png"); + + const restoredHead = execFileSync("git", ["rev-parse", "HEAD"], { + cwd: backingWorktree.workspaceDirectory, + encoding: "utf8", + }).trim(); + const defaultHead = execFileSync("git", ["rev-parse", "main"], { + cwd: tempRepo.path, + encoding: "utf8", + }).trim(); + expect(restoredHead).toBe(defaultHead); + expect( + execFileSync("git", ["branch", "--show-current"], { + cwd: backingWorktree.workspaceDirectory, + encoding: "utf8", + }).trim(), + ).toMatch(/^restored\//); + }); + test("restore failure stays visible and permits a successful retry", async ({ page }) => { const { agent, worktree } = await openArchivedWorkspaceFromHistory(page, "restore-retry"); const displacedProjectPath = `${tempRepo.path}-temporarily-unavailable`; @@ -422,6 +505,12 @@ test.describe("Worktree restore", () => { }); }); +async function captureBranchlessRestoreStep(page: Page, filename: string): Promise { + const screenshotDirectory = path.join(tmpdir(), "paseo-branchless-restore-evidence"); + mkdirSync(screenshotDirectory, { recursive: true }); + await page.screenshot({ path: path.join(screenshotDirectory, filename), fullPage: true }); +} + function commitRestorableChange(cwd: string): void { writeFileSync(path.join(cwd, "restored-change.txt"), "preserved after restore\n"); execFileSync("git", ["add", "restored-change.txt"], { cwd, stdio: "pipe" }); diff --git a/packages/app/src/i18n/resources/ar.ts b/packages/app/src/i18n/resources/ar.ts index a418df723f..c2a1475776 100644 --- a/packages/app/src/i18n/resources/ar.ts +++ b/packages/app/src/i18n/resources/ar.ts @@ -439,7 +439,9 @@ export const ar: TranslationResources = { recovery: { archivedTitle: "مساحة العمل مؤرشفة", restoreDescription: - "تمت أرشفة {{workspaceName}} وإزالة شجرة العمل الخاصة بها. استعد الفرع {{branch}} لفتحها مجددًا.", + "استعد {{workspaceName}} للعودة إلى وكلائها. ستستخدم شجرة العمل الفرع {{branch}}.", + restoreWithoutBranchDescription: + "استعد {{workspaceName}} للعودة إلى وكلائها. سيبدأ فرع جديد من الفرع الأساسي المحفوظ أو الفرع الافتراضي للمستودع.", unarchiveDescription: "{{workspaceName}} مؤرشفة. ألغِ أرشفتها لفتحها مجددًا.", restoreAction: "استعادة", unarchiveAction: "إلغاء الأرشفة", diff --git a/packages/app/src/i18n/resources/en.ts b/packages/app/src/i18n/resources/en.ts index 09774ae914..28600d410c 100644 --- a/packages/app/src/i18n/resources/en.ts +++ b/packages/app/src/i18n/resources/en.ts @@ -436,7 +436,9 @@ export const en = { recovery: { archivedTitle: "Workspace archived", restoreDescription: - "{{workspaceName}} was archived and its worktree was removed. Restore branch {{branch}} to open it again.", + "Restore {{workspaceName}} to return to its agents. Its worktree will use branch {{branch}}.", + restoreWithoutBranchDescription: + "Restore {{workspaceName}} to return to its agents. A new branch will start from the saved base or the repository default.", unarchiveDescription: "{{workspaceName}} is archived. Unarchive it to open it again.", restoreAction: "Restore", unarchiveAction: "Unarchive", diff --git a/packages/app/src/i18n/resources/es.ts b/packages/app/src/i18n/resources/es.ts index 3227988de9..7b9cf848df 100644 --- a/packages/app/src/i18n/resources/es.ts +++ b/packages/app/src/i18n/resources/es.ts @@ -444,7 +444,9 @@ export const es: TranslationResources = { recovery: { archivedTitle: "Espacio de trabajo archivado", restoreDescription: - "{{workspaceName}} se archivó y se eliminó su worktree. Restaura la rama {{branch}} para volver a abrirlo.", + "Restaura {{workspaceName}} para volver a sus agentes. Su worktree usará la rama {{branch}}.", + restoreWithoutBranchDescription: + "Restaura {{workspaceName}} para volver a sus agentes. Se creará una rama nueva desde la base guardada o la rama predeterminada del repositorio.", unarchiveDescription: "{{workspaceName}} está archivado. Desarchívalo para volver a abrirlo.", restoreAction: "Restaurar", diff --git a/packages/app/src/i18n/resources/fr.ts b/packages/app/src/i18n/resources/fr.ts index ca3343000f..15b8cc9944 100644 --- a/packages/app/src/i18n/resources/fr.ts +++ b/packages/app/src/i18n/resources/fr.ts @@ -445,7 +445,9 @@ export const fr: TranslationResources = { recovery: { archivedTitle: "Espace de travail archivé", restoreDescription: - "{{workspaceName}} a été archivé et son worktree supprimé. Restaurez la branche {{branch}} pour le rouvrir.", + "Restaurez {{workspaceName}} pour retrouver ses agents. Son worktree utilisera la branche {{branch}}.", + restoreWithoutBranchDescription: + "Restaurez {{workspaceName}} pour retrouver ses agents. Une nouvelle branche partira de la base enregistrée ou de la branche par défaut du dépôt.", unarchiveDescription: "{{workspaceName}} est archivé. Désarchivez-le pour le rouvrir.", restoreAction: "Restaurer", unarchiveAction: "Désarchiver", diff --git a/packages/app/src/i18n/resources/ja.ts b/packages/app/src/i18n/resources/ja.ts index 31c4c8fb61..6ccbb538b5 100644 --- a/packages/app/src/i18n/resources/ja.ts +++ b/packages/app/src/i18n/resources/ja.ts @@ -444,7 +444,9 @@ export const ja: TranslationResources = { recovery: { archivedTitle: "ワークスペースはアーカイブ済みです", restoreDescription: - "{{workspaceName}} はアーカイブされ、worktree が削除されました。ブランチ {{branch}} を復元して再度開きます。", + "{{workspaceName}} を復元してエージェントに戻ります。worktree ではブランチ {{branch}} を使用します。", + restoreWithoutBranchDescription: + "{{workspaceName}} を復元してエージェントに戻ります。保存されたベース、またはリポジトリのデフォルトブランチから新しいブランチを作成します。", unarchiveDescription: "{{workspaceName}} はアーカイブされています。再度開くにはアーカイブを解除してください。", restoreAction: "復元", diff --git a/packages/app/src/i18n/resources/ko.ts b/packages/app/src/i18n/resources/ko.ts index cf5ca5ea41..76dfb606ad 100644 --- a/packages/app/src/i18n/resources/ko.ts +++ b/packages/app/src/i18n/resources/ko.ts @@ -440,7 +440,9 @@ export const ko: TranslationResources = { recovery: { archivedTitle: "워크스페이스가 보관되었습니다", restoreDescription: - "{{workspaceName}}가 보관되고 워크트리가 제거되었습니다. 다시 열려면 {{branch}} 브랜치를 복원하세요.", + "{{workspaceName}}를 복원하여 에이전트로 돌아갑니다. 워크트리는 {{branch}} 브랜치를 사용합니다.", + restoreWithoutBranchDescription: + "{{workspaceName}}를 복원하여 에이전트로 돌아갑니다. 저장된 기반 브랜치 또는 저장소 기본 브랜치에서 새 브랜치를 만듭니다.", unarchiveDescription: "{{workspaceName}}가 보관되었습니다. 다시 열려면 보관을 취소하세요.", restoreAction: "복원", unarchiveAction: "보관 취소", diff --git a/packages/app/src/i18n/resources/pt-BR.ts b/packages/app/src/i18n/resources/pt-BR.ts index 5d4e10c14e..0c747106c6 100644 --- a/packages/app/src/i18n/resources/pt-BR.ts +++ b/packages/app/src/i18n/resources/pt-BR.ts @@ -443,7 +443,9 @@ export const ptBR: TranslationResources = { recovery: { archivedTitle: "Workspace arquivado", restoreDescription: - "{{workspaceName}} foi arquivado e sua worktree foi removida. Restaure a branch {{branch}} para abri-lo novamente.", + "Restaure {{workspaceName}} para voltar aos seus agentes. A worktree usará a branch {{branch}}.", + restoreWithoutBranchDescription: + "Restaure {{workspaceName}} para voltar aos seus agentes. Uma nova branch partirá da base salva ou da branch padrão do repositório.", unarchiveDescription: "{{workspaceName}} está arquivado. Desarquive-o para abri-lo novamente.", restoreAction: "Restaurar", diff --git a/packages/app/src/i18n/resources/ru.ts b/packages/app/src/i18n/resources/ru.ts index 271eafaa28..7e3fe4f436 100644 --- a/packages/app/src/i18n/resources/ru.ts +++ b/packages/app/src/i18n/resources/ru.ts @@ -443,7 +443,9 @@ export const ru: TranslationResources = { recovery: { archivedTitle: "Рабочая область в архиве", restoreDescription: - "Рабочее пространство «{{workspaceName}}» было архивировано, а его worktree удалён. Восстановите ветку {{branch}}, чтобы снова открыть рабочее пространство.", + "Восстановите «{{workspaceName}}», чтобы вернуться к его агентам. Worktree будет использовать ветку {{branch}}.", + restoreWithoutBranchDescription: + "Восстановите «{{workspaceName}}», чтобы вернуться к его агентам. Новая ветка будет создана от сохранённой базовой или ветки репозитория по умолчанию.", unarchiveDescription: "Рабочее пространство «{{workspaceName}}» находится в архиве. Разархивируйте его, чтобы снова открыть.", restoreAction: "Восстановить", diff --git a/packages/app/src/i18n/resources/zh-CN.ts b/packages/app/src/i18n/resources/zh-CN.ts index cdeb534e58..c54dbae802 100644 --- a/packages/app/src/i18n/resources/zh-CN.ts +++ b/packages/app/src/i18n/resources/zh-CN.ts @@ -439,7 +439,9 @@ export const zhCN: TranslationResources = { recovery: { archivedTitle: "Workspace 已归档", restoreDescription: - "{{workspaceName}} 已归档,其 worktree 已移除。恢复分支 {{branch}} 以重新打开。", + "恢复 {{workspaceName}} 以返回其中的代理。worktree 将使用分支 {{branch}}。", + restoreWithoutBranchDescription: + "恢复 {{workspaceName}} 以返回其中的代理。新分支将从保存的基准分支或仓库默认分支创建。", unarchiveDescription: "{{workspaceName}} 已归档。取消归档以重新打开。", restoreAction: "恢复", unarchiveAction: "取消归档", diff --git a/packages/app/src/screens/workspace/workspace-route-state-views.tsx b/packages/app/src/screens/workspace/workspace-route-state-views.tsx index 364816114f..f5f8c5e096 100644 --- a/packages/app/src/screens/workspace/workspace-route-state-views.tsx +++ b/packages/app/src/screens/workspace/workspace-route-state-views.tsx @@ -126,15 +126,21 @@ function ArchivedWorkspaceRecovery({ if (recovery.phase === "failed") { actionLabel = t("common.actions.retry"); } - const description = - recovery.recovery.action === "restore" - ? t("workspace.route.recovery.restoreDescription", { - workspaceName: recovery.recovery.workspaceName, - branch: recovery.recovery.branch, - }) - : t("workspace.route.recovery.unarchiveDescription", { - workspaceName: recovery.recovery.workspaceName, - }); + let description: string; + if (recovery.recovery.action === "restore" && recovery.recovery.branch) { + description = t("workspace.route.recovery.restoreDescription", { + workspaceName: recovery.recovery.workspaceName, + branch: recovery.recovery.branch, + }); + } else if (recovery.recovery.action === "restore") { + description = t("workspace.route.recovery.restoreWithoutBranchDescription", { + workspaceName: recovery.recovery.workspaceName, + }); + } else { + description = t("workspace.route.recovery.unarchiveDescription", { + workspaceName: recovery.recovery.workspaceName, + }); + } return ( diff --git a/packages/server/src/server/resolve-worktree-creation-intent.ts b/packages/server/src/server/resolve-worktree-creation-intent.ts index b966a6b8aa..73403f5908 100644 --- a/packages/server/src/server/resolve-worktree-creation-intent.ts +++ b/packages/server/src/server/resolve-worktree-creation-intent.ts @@ -1,7 +1,10 @@ import type { ForgeService, PullRequestCheckoutTarget } from "../services/forge-service.js"; import type { WorktreeSource } from "../utils/worktree.js"; -export type WorktreeCreationIntent = Exclude; +export type WorktreeCreationIntent = Exclude< + WorktreeSource, + { kind: "restore" | "restore-from-base" } +>; export interface ResolveWorktreeCreationIntentInput { worktreeSlug?: string; diff --git a/packages/server/src/server/session/workspace-recovery/workspace-recovery-service.test.ts b/packages/server/src/server/session/workspace-recovery/workspace-recovery-service.test.ts index 82ffafa4d7..964f305eb8 100644 --- a/packages/server/src/server/session/workspace-recovery/workspace-recovery-service.test.ts +++ b/packages/server/src/server/session/workspace-recovery/workspace-recovery-service.test.ts @@ -357,6 +357,67 @@ test("preserves ordinary checkout behavior when no separate base was recorded", ).toMatchObject({ currentBranch: "feature", baseRef: "feature" }); }); +test("restores a branchless workspace from its recorded base so work can continue", async () => { + const fixture = await createBaseRecoveryFixture("refs/heads/release"); + execFileSync("git", ["switch", "-c", "release"], { cwd: fixture.repoDir, stdio: "pipe" }); + writeFileSync(join(fixture.repoDir, "release.txt"), "release\n"); + execFileSync("git", ["add", "release.txt"], { cwd: fixture.repoDir, stdio: "pipe" }); + execFileSync("git", ["commit", "-m", "release"], { cwd: fixture.repoDir, stdio: "pipe" }); + execFileSync("git", ["switch", "main"], { cwd: fixture.repoDir, stdio: "pipe" }); + await fixture.registry.update(fixture.workspace.workspaceId, (workspace) => ({ + ...workspace, + branch: null, + })); + await fixture.archiveAndRemove(); + + await expect(fixture.service.inspect(fixture.workspace.workspaceId)).resolves.toMatchObject({ + kind: "recoverable", + action: "restore", + branch: null, + }); + await expect(fixture.service.restore(fixture.workspace.workspaceId)).resolves.toEqual({ + workspaceId: fixture.workspace.workspaceId, + action: "restore", + }); + + const checkout = await getCheckoutStatus(fixture.workspace.cwd, { paseoHome: fixture.paseoHome }); + expect(checkout.currentBranch).toBeTruthy(); + expect(checkout.currentBranch).not.toBe("feature"); + expect( + execFileSync("git", ["rev-parse", "HEAD"], { + cwd: fixture.workspace.cwd, + encoding: "utf8", + }).trim(), + ).toBe( + execFileSync("git", ["rev-parse", "release"], { + cwd: fixture.repoDir, + encoding: "utf8", + }).trim(), + ); +}); + +test("restores a branchless workspace from the repository default when its base is missing", async () => { + const fixture = await createBaseRecoveryFixture("refs/heads/deleted-base"); + await fixture.registry.update(fixture.workspace.workspaceId, (workspace) => ({ + ...workspace, + branch: null, + })); + await fixture.archiveAndRemove(); + + await expect(fixture.service.restore(fixture.workspace.workspaceId)).resolves.toEqual({ + workspaceId: fixture.workspace.workspaceId, + action: "restore", + }); + expect( + execFileSync("git", ["rev-parse", "HEAD"], { + cwd: fixture.workspace.cwd, + encoding: "utf8", + }).trim(), + ).toBe( + execFileSync("git", ["rev-parse", "main"], { cwd: fixture.repoDir, encoding: "utf8" }).trim(), + ); +}); + test("restores the branch HEAD with a base name when the exact base is missing", async () => { const fixture = await createBaseRecoveryFixture("refs/remotes/upstream/main"); const headBeforeArchive = execFileSync("git", ["rev-parse", "HEAD"], { diff --git a/packages/server/src/server/session/workspace-recovery/workspace-recovery-service.ts b/packages/server/src/server/session/workspace-recovery/workspace-recovery-service.ts index f40e0e8afc..8b0e9a853a 100644 --- a/packages/server/src/server/session/workspace-recovery/workspace-recovery-service.ts +++ b/packages/server/src/server/session/workspace-recovery/workspace-recovery-service.ts @@ -1,5 +1,7 @@ +import { randomUUID } from "node:crypto"; import { basename } from "node:path"; +import { resolveRepositoryDefaultBranch } from "../../../utils/checkout-git.js"; import { createRealpathAwarePathMatcher } from "../../../utils/path.js"; import { runGitCommand } from "../../../utils/run-git-command.js"; import { @@ -7,6 +9,7 @@ import { isPaseoOwnedWorktreeCwd, mapWorkspaceCwdToWorktree, rollbackCreatedPaseoWorktree, + type WorktreeSource, } from "../../../utils/worktree.js"; import { WorktreeRequestError, toWorktreeRequestError } from "../../worktree-errors.js"; import { @@ -109,15 +112,6 @@ export function createWorkspaceRecoveryService(deps: { message: "The archived workspace directory no longer exists and cannot be recreated.", }; } - if (!workspace.branch) { - return { - kind: "unavailable", - workspaceId, - reason: "worktree_branch_missing", - message: "The archived worktree has no branch recorded, so it cannot be restored.", - }; - } - // COMPAT(worktreeRestoreMissingMainRepoRoot): records created before v0.1.110 // lack placement ownership; remove the project-root fallback after 2027-01-17. const sourceRepoRoot = workspace.mainRepoRoot ?? project.rootPath; @@ -157,13 +151,13 @@ export function createWorkspaceRecoveryService(deps: { workspace: PersistedWorkspaceRecord, sourceRepoRoot: string, ): Promise { - const branch = workspace.branch; - if (!branch) { - throw new WorktreeRequestError({ - code: "unknown", - message: `Workspace ${workspace.workspaceId} has no branch to restore`, - }); - } + const source: WorktreeSource = workspace.branch + ? { kind: "restore", branchName: workspace.branch, baseRef: workspace.baseBranch } + : { + kind: "restore-from-base", + branchName: `restored/${randomUUID().slice(0, 8)}`, + baseRef: await resolveRecoveryBase(sourceRepoRoot, workspace.baseBranch), + }; try { await runGitCommand(["worktree", "prune"], { cwd: sourceRepoRoot, timeout: 30_000 }); @@ -189,7 +183,7 @@ export function createWorkspaceRecoveryService(deps: { const result = await createWorktree({ cwd: sourceRepoRoot, worktreeSlug: basename(previousWorktreePath), - source: { kind: "restore", branchName: branch, baseRef: workspace.baseBranch }, + source, runSetup: false, paseoHome: deps.paseoHome, worktreesRoot: deps.worktreesRoot, @@ -234,6 +228,26 @@ export function createWorkspaceRecoveryService(deps: { return { inspect, restore }; } +async function resolveRecoveryBase(repoRoot: string, recordedBase: string | null): Promise { + if (recordedBase) { + try { + await runGitCommand(["rev-parse", "--verify", `${recordedBase}^{commit}`], { cwd: repoRoot }); + return recordedBase; + } catch { + // The saved base was removed; use the repository default for continued work. + } + } + const defaultBranch = await resolveRepositoryDefaultBranch(repoRoot); + if (!defaultBranch) { + throw new WorktreeRequestError({ + code: "unknown", + message: + "The saved base is unavailable and the repository has no default branch to restore from.", + }); + } + return defaultBranch; +} + function createRecoveryPlan( input: | { action: "unarchive"; workspace: PersistedWorkspaceRecord } diff --git a/packages/server/src/utils/worktree.ts b/packages/server/src/utils/worktree.ts index ad5ba0f8b3..0fd83e8dc7 100644 --- a/packages/server/src/utils/worktree.ts +++ b/packages/server/src/utils/worktree.ts @@ -186,6 +186,7 @@ export type WorktreeSource = | { kind: "branch-off"; baseBranch: string; branchName: string } | { kind: "checkout-branch"; branchName: string } | { kind: "restore"; branchName: string; baseRef: string | null } + | { kind: "restore-from-base"; baseRef: string; branchName: string } | { kind: "checkout-change-request"; forge: string; @@ -1341,6 +1342,21 @@ async function resolveRestoredWorktreeSourcePlan( }; } +async function resolveRestoredWorktreeFromBasePlan( + cwd: string, + source: Extract, +): Promise { + await validateGitBranchName(cwd, source.branchName); + const baseRef = await resolveBaseBranchForWorktree(cwd, source.baseRef); + const branchName = await resolveUniqueLocalBranchName(cwd, source.branchName); + return { + branchName, + metadataBaseRefName: normalizeRequiredBaseBranch(source.baseRef), + metadataBaseRef: baseRef, + addArguments: ["-b", branchName, "--no-track", baseRef], + }; +} + async function resolveBranchOffWorktreeSourcePlan( cwd: string, source: Extract, @@ -1377,6 +1393,8 @@ async function resolveWorktreeSourcePlan({ return resolveBranchOffWorktreeSourcePlan(cwd, source, desiredSlug); case "restore": return resolveRestoredWorktreeSourcePlan(cwd, source); + case "restore-from-base": + return resolveRestoredWorktreeFromBasePlan(cwd, source); case "checkout-branch": { await validateGitBranchName(cwd, source.branchName); await ensureLocalBranch(cwd, source.branchName); From 5764863f63f5877cc88dcadf1e40434752c30957 Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Tue, 29 Sep 2026 19:15:39 +0200 Subject: [PATCH 004/100] fix(opencode): show v2 patch edits as edit diffs (#5696) OpenCode 2 reports GPT models' apply_patch tool calls under the name patch. The OpenCode tool-call parser only recognized apply_patch, so these calls fell through to an unknown tool card with the raw patch text. Map patch payloads the same way as apply_patch. Closes #5643 Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> --- .../opencode/tool-call-detail-parser.ts | 4 +++ .../opencode/tool-call-mapper.test.ts | 28 +++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/packages/server/src/server/agent/providers/opencode/tool-call-detail-parser.ts b/packages/server/src/server/agent/providers/opencode/tool-call-detail-parser.ts index 39a7deb8c9..8214ebb1d0 100644 --- a/packages/server/src/server/agent/providers/opencode/tool-call-detail-parser.ts +++ b/packages/server/src/server/agent/providers/opencode/tool-call-detail-parser.ts @@ -312,6 +312,10 @@ const OpencodeKnownToolDetailSchema = z.union([ z.unknown(), (input) => toEditToolDetail(input, null), ), + // OpenCode v2 reports apply_patch as `patch`. + toolDetailBranchByToolName("patch", OpencodeApplyPatchTextInputSchema, z.unknown(), (input) => + toEditToolDetail(input, null), + ), toolDetailBranchByToolName( "apply_diff", OpencodeEditInputSchema, diff --git a/packages/server/src/server/agent/providers/opencode/tool-call-mapper.test.ts b/packages/server/src/server/agent/providers/opencode/tool-call-mapper.test.ts index 54f3faa514..1c9ba212e1 100644 --- a/packages/server/src/server/agent/providers/opencode/tool-call-mapper.test.ts +++ b/packages/server/src/server/agent/providers/opencode/tool-call-mapper.test.ts @@ -387,6 +387,34 @@ describe("opencode tool-call mapper", () => { }); }); + it("maps OpenCode v2 patch payloads from GPT models into edit detail", () => { + const item = expectMapped( + mapOpencodeToolCall({ + toolName: "patch", + callId: "opencode-v2-patch-1", + status: "completed", + input: { + patchText: + "*** Begin Patch\n*** Update File: patch.txt\n@@\n-north south\n+NORTH south\n*** End Patch", + }, + output: "Success. Updated the following files:\nM patch.txt", + }), + ); + + expect(item.detail).toEqual({ + type: "edit", + filePath: "patch.txt", + unifiedDiff: [ + "diff --git a/patch.txt b/patch.txt", + "--- a/patch.txt", + "+++ b/patch.txt", + "@@", + "-north south", + "+NORTH south", + ].join("\n"), + }); + }); + it("maps unknown tools to unknown detail with raw payloads", () => { const item = expectMapped( mapOpencodeToolCall({ From 5de45e208690b0efc51c59a585ae9729325a9204 Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Tue, 29 Sep 2026 20:08:05 +0200 Subject: [PATCH 005/100] fix(desktop): capture the annotated element when the app is zoomed (#5703) The browser pane measures the selected element in page CSS pixels, but capturePage takes device-independent pixels. Paseo's zoom carries into the in-app browser, so at any zoom other than 100% the screenshot attached to an annotation was offset and undersized. Scale the region by the guest's zoom factor before capturing. Closes #5690 Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> --- .../src/features/browser-capture.test.ts | 20 +++++++++++++++++-- .../desktop/src/features/browser-capture.ts | 14 +++++++++---- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/packages/desktop/src/features/browser-capture.test.ts b/packages/desktop/src/features/browser-capture.test.ts index 5c7d16a77a..ea377f51c5 100644 --- a/packages/desktop/src/features/browser-capture.test.ts +++ b/packages/desktop/src/features/browser-capture.test.ts @@ -29,7 +29,7 @@ function harness(guest: BrowserCaptureGuest | null = null) { describe("browser capture service", () => { it("validates and rounds guest-relative bounds before capture", async () => { const capturePage = vi.fn(async () => image()); - const { service } = harness({ isDestroyed: () => false, capturePage }); + const { service } = harness({ isDestroyed: () => false, getZoomFactor: () => 1, capturePage }); await expect( service.capture({ @@ -41,9 +41,25 @@ describe("browser capture service", () => { expect(capturePage).toHaveBeenCalledWith({ x: 0, y: 9, width: 20, height: 11 }); }); + it("captures the selected element when the page is zoomed", async () => { + const capturePage = vi.fn(async () => image()); + const { service } = harness({ + isDestroyed: () => false, + getZoomFactor: () => 1.2, + capturePage, + }); + + await service.capture({ + browserId: "browser-1", + hostWebContentsId: 42, + rect: { x: 300, y: 200, width: 120, height: 60 }, + }); + expect(capturePage).toHaveBeenCalledWith({ x: 360, y: 240, width: 144, height: 72 }); + }); + it("rejects invalid or unavailable captures without touching the guest", async () => { const capturePage = vi.fn(async () => image()); - const { service } = harness({ isDestroyed: () => false, capturePage }); + const { service } = harness({ isDestroyed: () => false, getZoomFactor: () => 1, capturePage }); await expect( service.capture({ browserId: "browser-1", hostWebContentsId: 42, rect: { width: 0 } }), diff --git a/packages/desktop/src/features/browser-capture.ts b/packages/desktop/src/features/browser-capture.ts index c2c7326ce7..199fde27e7 100644 --- a/packages/desktop/src/features/browser-capture.ts +++ b/packages/desktop/src/features/browser-capture.ts @@ -5,6 +5,7 @@ export interface BrowserCaptureImage { export interface BrowserCaptureGuest { isDestroyed(): boolean; + getZoomFactor(): number; capturePage(rect: BrowserCaptureRect): Promise; } @@ -40,7 +41,9 @@ export interface BrowserCaptureService { copy(payload: unknown): Promise; } -function captureRect(value: unknown): BrowserCaptureRect | null { +// The renderer measures the element in page CSS pixels, while capturePage +// takes device-independent pixels, so the page zoom scales the region. +function captureRect(value: unknown, zoomFactor: number): BrowserCaptureRect | null { if (!value || typeof value !== "object") return null; const record = value as Record; const coordinates = [record.x, record.y, record.width, record.height]; @@ -51,7 +54,9 @@ function captureRect(value: unknown): BrowserCaptureRect | null { ) { return null; } - const [x, y, width, height] = coordinates as number[]; + const [x, y, width, height] = (coordinates as number[]).map( + (coordinate) => coordinate * zoomFactor, + ); if (width <= 0 || height <= 0) return null; return { x: Math.max(0, Math.round(x)), @@ -80,8 +85,9 @@ export function createBrowserCaptureService( async capture({ browserId, hostWebContentsId, rect }) { if (typeof browserId !== "string" || browserId.trim().length === 0) return null; const guest = dependencies.findGuest(browserId, hostWebContentsId); - const bounds = captureRect(rect); - if (!guest || guest.isDestroyed() || !bounds) return null; + if (!guest || guest.isDestroyed()) return null; + const bounds = captureRect(rect, guest.getZoomFactor()); + if (!bounds) return null; try { const image = await guest.capturePage(bounds); return image.isEmpty() ? null : image.toDataURL(); From 60aff2346c488528b44a9e970a71d8ad660e36e6 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Tue, 29 Sep 2026 20:19:59 +0100 Subject: [PATCH 006/100] Support catalog-driven Codex Fast and Ultrafast speeds (#5708) * feat(codex): discover Fast and Ultrafast speeds from the catalog Replace the model allowlist with advertised service tiers so new models and speeds need no Paseo catalog update. Normal explicitly overrides the thread tier. Desktop icon-only triggers are provider metadata; mobile selectors remain labeled. * chore: remove QA screenshots from the change --- packages/app/e2e/browser/codex-speed.spec.ts | 122 ++++++++ packages/app/e2e/fixtures/catalog-codex.mjs | 88 ++++++ .../app/src/composer/agent-controls/index.tsx | 9 +- .../src/agent-feature-schemas.test.ts | 16 + packages/protocol/src/agent-types.ts | 2 + packages/protocol/src/messages.ts | 1 + .../src/server/agent/agent-sdk-types.ts | 34 +-- .../codex-app-server-agent.features.test.ts | 283 ++++++++++++++---- .../providers/codex-app-server-agent.test.ts | 8 +- .../agent/providers/codex-app-server-agent.ts | 80 +++-- .../providers/codex-feature-definitions.ts | 88 +++--- 11 files changed, 575 insertions(+), 156 deletions(-) create mode 100644 packages/app/e2e/browser/codex-speed.spec.ts create mode 100644 packages/app/e2e/fixtures/catalog-codex.mjs diff --git a/packages/app/e2e/browser/codex-speed.spec.ts b/packages/app/e2e/browser/codex-speed.spec.ts new file mode 100644 index 0000000000..654f15207d --- /dev/null +++ b/packages/app/e2e/browser/codex-speed.spec.ts @@ -0,0 +1,122 @@ +import path from "node:path"; +import { expect, test, type Page } from "../support/fixtures"; +import { seedWorkspace } from "../support/helpers/seed-client"; +import { openAgentRoute } from "../support/helpers/mock-agent"; +import type { DaemonClient } from "@getpaseo/client/internal/daemon-client"; +import { connectDaemonClient } from "../support/helpers/daemon-client-loader"; + +// This fixture catalog advertises Ultrafast on Sol; another model offers Fast only. +// The real daemon discovers both through the same Codex model/list boundary. +test.use({ + e2eDaemonConfig: { + version: 1, + agents: { + providers: { + codex: { + enabled: true, + command: [process.execPath, path.resolve("e2e/fixtures/catalog-codex.mjs")], + }, + claude: { enabled: false }, + copilot: { enabled: false }, + opencode: { enabled: false }, + pi: { enabled: false }, + omp: { enabled: false }, + }, + }, + }, +}); + +async function openSpeedSelector(page: Page): Promise { + const speed = page.getByRole("button", { name: /^(Select speed|Speed: .+)$/ }); + const features = page.getByRole("button", { name: "Open agent features", exact: true }); + const modelSettings = page.getByRole("button", { name: /^Select model \(/ }); + await expect(speed.or(features).or(modelSettings).filter({ visible: true }).first()).toBeVisible({ + timeout: 30_000, + }); + if (!(await speed.isVisible())) { + if (await features.isVisible()) await features.click(); + else await modelSettings.click(); + } + await expect(speed).toBeVisible({ timeout: 30_000 }); + await speed.click(); + await expect(page.getByText("Normal", { exact: true }).last()).toBeVisible(); +} + +async function selectSpeed(page: Page, label: string): Promise { + await page.getByRole("button", { name: label, exact: true }).last().click(); + const toolbarTrigger = page.getByRole("button", { name: `Speed: ${label}`, exact: true }); + const sheetTrigger = page.getByRole("button", { name: "Select speed", exact: true }); + await expect(toolbarTrigger.or(sheetTrigger)).toBeVisible(); + if (await toolbarTrigger.isVisible()) await expect(toolbarTrigger).toHaveText(""); + else await expect(sheetTrigger).toContainText(label); +} + +async function expectSpeedChoices(page: Page, choices: string[]): Promise { + for (const choice of ["Normal", "Fast", "Ultrafast"]) { + const option = page.getByText(choice, { exact: true }).last(); + if (choices.includes(choice)) { + await expect(option).toBeVisible(); + await expect(option).toBeInViewport({ ratio: 1 }); + } else await expect(option).not.toBeVisible(); + } +} + +async function switchAgentModel(agentId: string, modelId: string): Promise { + const client = await connectDaemonClient< + Pick + >({ + clientIdPrefix: "codex-speed-model", + }); + try { + await client.setAgentModel(agentId, modelId); + } finally { + await client.close(); + } +} + +for (const viewport of [ + { name: "desktop", width: 1280, height: 850 }, + { name: "compact", width: 390, height: 844 }, +]) { + test(`Codex catalog drives speed choices on ${viewport.name}`, async ({ page }, testInfo) => { + test.setTimeout(180_000); + await page.setViewportSize(viewport); + const workspace = await seedWorkspace({ repoPrefix: "codex-speed-" }); + try { + const agent = await workspace.client.createAgent({ + provider: "codex", + cwd: workspace.repoPath, + workspaceId: workspace.workspaceId, + title: "Speed settings", + model: "gpt-6.1-sol", + modeId: "full-access", + initialPrompt: "Get ready to work on this project.", + }); + await openAgentRoute(page, { workspaceId: workspace.workspaceId, agentId: agent.id }); + if (viewport.name === "desktop") { + const trigger = page.getByRole("button", { name: "Speed: Normal", exact: true }); + await expect(trigger).toBeVisible({ timeout: 30_000 }); + await expect(trigger).toHaveText(""); + await page.screenshot({ path: testInfo.outputPath("desktop-normal-trigger.png") }); + } + await openSpeedSelector(page); + await expectSpeedChoices(page, ["Normal", "Fast", "Ultrafast"]); + await page.screenshot({ path: testInfo.outputPath(`${viewport.name}-speed-options.png`) }); + await selectSpeed(page, "Ultrafast"); + await openSpeedSelector(page); + await expectSpeedChoices(page, ["Normal", "Fast", "Ultrafast"]); + await page.screenshot({ + path: testInfo.outputPath(`${viewport.name}-ultrafast-selected.png`), + }); + await selectSpeed(page, "Normal"); + if (viewport.name === "desktop") { + await switchAgentModel(agent.id, "gpt-6-sol"); + await openSpeedSelector(page); + await expectSpeedChoices(page, ["Normal", "Fast"]); + await page.screenshot({ path: testInfo.outputPath("desktop-fast-only.png") }); + } + } finally { + await workspace.cleanup(); + } + }); +} diff --git a/packages/app/e2e/fixtures/catalog-codex.mjs b/packages/app/e2e/fixtures/catalog-codex.mjs new file mode 100644 index 0000000000..1b3826cb30 --- /dev/null +++ b/packages/app/e2e/fixtures/catalog-codex.mjs @@ -0,0 +1,88 @@ +import readline from "node:readline"; +import { randomUUID } from "node:crypto"; + +const fast = { id: "priority", name: "Fast", description: "Faster processing, increased usage" }; +const ultra = { + id: "ultrafast", + name: "Ultrafast", + description: "Lowest latency, increased usage", +}; +const models = [ + { id: "gpt-6.1-sol", displayName: "GPT-6.1 Sol", isDefault: true, serviceTiers: [fast, ultra] }, + { id: "gpt-6-sol", displayName: "GPT-6 Sol", serviceTiers: [fast] }, +].map((model) => + Object.assign(model, { + model: model.id, + defaultReasoningEffort: "medium", + supportedReasoningEfforts: [{ reasoningEffort: "medium", description: "Medium" }], + }), +); + +function send(message) { + process.stdout.write(JSON.stringify(message) + "\n"); +} +function respond(method, params) { + switch (method) { + case "initialize": + return {}; + case "model/list": + return { data: models }; + case "config/read": + return { config: { model: models[0].id, model_reasoning_effort: "medium" } }; + case "collaborationMode/list": + return { data: [] }; + case "skills/list": + return { data: [] }; + case "thread/list": + case "thread/loaded/list": + return { data: [] }; + case "thread/start": + return { thread: { id: randomUUID() }, model: params.model }; + case "thread/resume": + return { thread: { id: params.threadId } }; + case "thread/read": + return { thread: { id: params.threadId, turns: [] } }; + case "turn/start": { + const turnId = randomUUID(); + setTimeout(() => { + send({ + method: "turn/started", + params: { threadId: params.threadId, turn: { id: turnId } }, + }); + send({ + method: "item/completed", + params: { + threadId: params.threadId, + turnId, + item: { + id: randomUUID(), + type: "agentMessage", + text: "Ready to work on this project.", + }, + }, + }); + send({ + method: "turn/completed", + params: { threadId: params.threadId, turn: { id: turnId, status: "completed" } }, + }); + }, 50); + return { turn: { id: turnId } }; + } + default: + throw new Error(`Unsupported Codex fixture request: ${method}`); + } +} + +if (process.argv.includes("--version")) { + console.log("codex-cli 0.159.0"); +} else { + readline.createInterface({ input: process.stdin }).on("line", (line) => { + const request = JSON.parse(line); + if (request.id === undefined) return; + try { + send({ id: request.id, result: respond(request.method, request.params ?? {}) }); + } catch (error) { + send({ id: request.id, error: { code: -32601, message: error.message } }); + } + }); +} diff --git a/packages/app/src/composer/agent-controls/index.tsx b/packages/app/src/composer/agent-controls/index.tsx index f873b42148..8dadeec217 100644 --- a/packages/app/src/composer/agent-controls/index.tsx +++ b/packages/app/src/composer/agent-controls/index.tsx @@ -1357,6 +1357,10 @@ function DesktopFeatureItem({ if (feature.type === "select") { const FeatureIcon = getAgentFeatureIcon(feature.icon); const selectedOption = feature.options.find((o) => o.id === feature.value); + const iconOnly = feature.desktopTrigger === "icon"; + const tooltip = iconOnly + ? `${feature.label}: ${selectedOption?.label ?? feature.label}` + : getFeatureTooltip(feature); return ( <> @@ -1367,15 +1371,16 @@ function DesktopFeatureItem({ surface="toolbar" label={feature.label} value={selectedOption?.label ?? feature.label} + showToolbarLabel={!iconOnly} open={openSelector === featureSelector} disabled={disabled} onPress={handleSelectPress} - accessibilityLabel={getFeatureTooltip(feature)} + accessibilityLabel={tooltip} testID={`agent-feature-${feature.id}`} /> - {getFeatureTooltip(feature)} + {tooltip} { expect(parsed.value).toBe("flex"); }); + it.each(["icon", "label"])( + "preserves the %s desktop trigger for any select feature", + (desktopTrigger) => { + const feature = { + type: "select", + id: "custom-feature", + label: "Custom setting", + icon: "gauge", + value: "first", + options: [{ id: "first", label: "First" }], + desktopTrigger, + }; + expect(AgentFeatureSchema.parse(feature)).toEqual(feature); + }, + ); + it("rejects invalid features", () => { const invalidDiscriminator = AgentFeatureSchema.safeParse({ type: "slider", diff --git a/packages/protocol/src/agent-types.ts b/packages/protocol/src/agent-types.ts index ea412c16be..f86dc63575 100644 --- a/packages/protocol/src/agent-types.ts +++ b/packages/protocol/src/agent-types.ts @@ -145,6 +145,8 @@ export interface AgentFeatureSelect { description?: string; tooltip?: string; icon?: string; + /** Desktop toolbar presentation; omitted means labeled. Mobile selectors stay labeled. */ + desktopTrigger?: "icon" | "label"; value: string | null; options: AgentSelectOption[]; } diff --git a/packages/protocol/src/messages.ts b/packages/protocol/src/messages.ts index a716d45893..589137d720 100644 --- a/packages/protocol/src/messages.ts +++ b/packages/protocol/src/messages.ts @@ -301,6 +301,7 @@ export const AgentFeatureSelectSchema = z.object({ description: z.string().optional(), tooltip: z.string().optional(), icon: z.string().optional(), + desktopTrigger: z.enum(["icon", "label"]).optional(), value: z.string().nullable(), options: z.array(AgentSelectOptionSchema), }); diff --git a/packages/server/src/server/agent/agent-sdk-types.ts b/packages/server/src/server/agent/agent-sdk-types.ts index d42d98aaf7..effd5157c1 100644 --- a/packages/server/src/server/agent/agent-sdk-types.ts +++ b/packages/server/src/server/agent/agent-sdk-types.ts @@ -1,4 +1,7 @@ import type { + AgentFeature, + AgentFeatureSelect, + AgentFeatureToggle, AgentProviderNotice, AgentTaskItem, JsonValue, @@ -8,7 +11,13 @@ import type { import type { AgentAttachment } from "@getpaseo/protocol/messages"; import type { PaseoToolCatalog } from "./tools/types.js"; -export type { AgentProviderNotice, AgentTaskItem }; +export type { + AgentFeature, + AgentFeatureSelect, + AgentFeatureToggle, + AgentProviderNotice, + AgentTaskItem, +}; export type AgentProvider = string; @@ -156,29 +165,6 @@ export interface AgentCreateConfigUnattendedInput { availableModes: AgentMode[]; } -export interface AgentFeatureToggle { - type: "toggle"; - id: string; - label: string; - description?: string; - tooltip?: string; - icon?: string; - value: boolean; -} - -export interface AgentFeatureSelect { - type: "select"; - id: string; - label: string; - description?: string; - tooltip?: string; - icon?: string; - value: string | null; - options: AgentSelectOption[]; -} - -export type AgentFeature = AgentFeatureToggle | AgentFeatureSelect; - export interface AgentCapabilityFlags { [capability: string]: boolean | undefined; supportsStreaming: boolean; diff --git a/packages/server/src/server/agent/providers/codex-app-server-agent.features.test.ts b/packages/server/src/server/agent/providers/codex-app-server-agent.features.test.ts index 2e20f5f6f1..8f3f8fc76d 100644 --- a/packages/server/src/server/agent/providers/codex-app-server-agent.features.test.ts +++ b/packages/server/src/server/agent/providers/codex-app-server-agent.features.test.ts @@ -19,6 +19,41 @@ interface CollaborationModeRecord { developer_instructions?: string | null; } +const TEST_SPEED_MODELS = [ + "gpt-6-astra", + "gpt-6.1-sol", + "gpt-6-sol", + "gpt-6-luna", + "gpt-5.6", + "gpt-5.6-sol", + "gpt-5.6-terra", + "gpt-5.6-luna", + "gpt-5.5", + "gpt-5.4", +].map((id) => ({ + id, + isDefault: id === "gpt-5.4", + defaultReasoningEffort: "medium", + serviceTiers: [{ id: "priority", name: "Fast", description: "Faster processing" }], +})); + +function speedFeature(value: string) { + return { + type: "select", + id: "service_tier", + label: "Speed", + description: "Choose processing speed. Faster tiers increase usage.", + tooltip: "Select speed", + icon: "zap", + desktopTrigger: "icon", + value, + options: [ + { id: "default", label: "Normal", isDefault: true }, + { id: "priority", label: "Fast" }, + ], + }; +} + const TEST_COLLABORATION_MODES: CollaborationModeRecord[] = [ { name: "Code", @@ -65,7 +100,7 @@ function createConfig(overrides: Partial = {}): AgentSession function createSessionHarness( configOverrides: Partial = {}, - options: { logger?: pino.Logger } = {}, + options: { logger?: pino.Logger; models?: unknown[] } = {}, ): { session: CodexFeaturesTestSession; appServer: FakeCodexAppServer; @@ -73,6 +108,7 @@ function createSessionHarness( const config = createConfig(configOverrides); const appServer = createFakeCodexAppServer({ "collaborationMode/list": () => ({ data: TEST_COLLABORATION_MODES }), + "model/list": () => ({ data: options.models ?? TEST_SPEED_MODELS }), }); const session = new CodexAppServerAgentSession( { ...config, provider: CODEX_PROVIDER }, @@ -85,7 +121,7 @@ function createSessionHarness( async function createConnectedSession( configOverrides: Partial = {}, - options: { logger?: pino.Logger } = {}, + options: { logger?: pino.Logger; models?: unknown[] } = {}, ): Promise<{ session: CodexFeaturesTestSession; appServer: FakeCodexAppServer; @@ -97,8 +133,169 @@ async function createConnectedSession( } describe("Codex app-server provider features", () => { + test("GPT-6.1 Sol offers catalog speed tiers and sends Ultrafast", async () => { + const { session, appServer } = await createConnectedSession( + { model: "gpt-6.1-sol" }, + { + models: [ + { + id: "gpt-6.1-sol", + serviceTiers: [ + { id: "priority", name: "Fast", description: "Faster processing" }, + { id: "ultrafast", name: "Ultrafast", description: "Lowest latency" }, + ], + }, + ], + }, + ); + try { + expect(session.features).toContainEqual( + expect.objectContaining({ + id: "service_tier", + type: "select", + value: "default", + options: [ + { id: "default", label: "Normal", isDefault: true }, + { id: "priority", label: "Fast" }, + { id: "ultrafast", label: "Ultrafast" }, + ], + }), + ); + await session.setFeature?.("service_tier", "ultrafast"); + await session.startTurn("hello"); + await expect(appServer.waitForTurnStart()).resolves.toMatchObject({ + serviceTier: "ultrafast", + }); + } finally { + await session.close(); + } + }); + + test("new models and service tiers work without a Paseo catalog update", async () => { + const { session, appServer } = await createConnectedSession( + { model: "future-codex-model" }, + { + models: [ + { + id: "future-codex-model", + serviceTiers: [ + { id: "future-tier", name: "Future speed", description: "A newly advertised tier" }, + ], + }, + ], + }, + ); + try { + expect(session.features).toContainEqual( + expect.objectContaining({ + id: "service_tier", + options: [ + { id: "default", label: "Normal", isDefault: true }, + { id: "future-tier", label: "Future speed" }, + ], + }), + ); + await session.setFeature?.("service_tier", "future-tier"); + await session.startTurn("hello"); + await expect(appServer.waitForTurnStart()).resolves.toMatchObject({ + model: "future-codex-model", + serviceTier: "future-tier", + }); + } finally { + await session.close(); + } + }); + + test.each(["priority", "ultrafast"])( + "restores %s and Normal explicitly overrides an existing thread tier", + async (tier) => { + const { session, appServer } = await createConnectedSession( + { model: "future-model", featureValues: { service_tier: tier } }, + { + models: [ + { + id: "future-model", + serviceTiers: [ + { id: "priority", name: "Fast", description: "Fast processing" }, + { id: "ultrafast", name: "Ultrafast", description: "Ultra processing" }, + ], + }, + ], + }, + ); + try { + expect(session.features).toContainEqual( + expect.objectContaining({ id: "service_tier", value: tier }), + ); + await session.getRuntimeInfo(); + await session.setFeature?.("service_tier", "default"); + await session.startTurn("hello"); + await expect(appServer.waitForTurnStart()).resolves.toMatchObject({ + serviceTier: "default", + }); + } finally { + await session.close(); + } + }, + ); + + test("rejects unadvertised tiers and non-string values", async () => { + const { session } = await createConnectedSession(); + try { + for (const value of ["ultrafast", "bogus", true, null]) { + await expect(session.setFeature?.("service_tier", value)).rejects.toThrow( + "is not available", + ); + } + expect(session.features).toContainEqual( + expect.objectContaining({ id: "service_tier", value: "default" }), + ); + } finally { + await session.close(); + } + }); + + test("switching to a model without the selected tier resets speed", async () => { + const { session, appServer } = await createConnectedSession( + { model: "ultra-model", featureValues: { service_tier: "ultrafast" } }, + { + models: [ + { + id: "ultra-model", + serviceTiers: [{ id: "ultrafast", name: "Ultrafast", description: "Ultra processing" }], + }, + ...TEST_SPEED_MODELS, + ], + }, + ); + try { + await session.setModel("gpt-6.1-sol"); + expect(session.features).toContainEqual( + expect.objectContaining({ id: "service_tier", value: "default" }), + ); + await session.startTurn("hello"); + await expect(appServer.waitForTurnStart()).resolves.toMatchObject({ serviceTier: "default" }); + } finally { + await session.close(); + } + }); + + test("maps legacy Fast commands to the catalog's request id", async () => { + const { session, appServer } = await createConnectedSession(); + try { + await session.setFeature?.("fast_mode", true); + await session.startTurn("hello"); + await expect(appServer.waitForTurnStart()).resolves.toMatchObject({ + serviceTier: "priority", + }); + } finally { + await session.close(); + } + }); + test.each([ "gpt-6-astra", + "gpt-6.1-sol", "gpt-6-sol", "gpt-6-luna", "gpt-5.6", @@ -112,15 +309,15 @@ describe("Codex app-server provider features", () => { try { expect(session.features).toContainEqual( expect.objectContaining({ - id: "fast_mode", - value: false, + id: "service_tier", + value: "default", }), ); - await session.setFeature?.("fast_mode", true); + await session.setFeature?.("service_tier", "priority"); await session.startTurn("hello"); await expect(appServer.waitForTurnStart()).resolves.toMatchObject({ model, - serviceTier: "fast", + serviceTier: "priority", }); } finally { await session.close(); @@ -149,8 +346,8 @@ describe("Codex app-server provider features", () => { `Codex fast mode is not available for model '${model}'`, ); await session.startTurn("hello"); - await expect(appServer.waitForTurnStart()).resolves.not.toMatchObject({ - serviceTier: expect.anything(), + await expect(appServer.waitForTurnStart()).resolves.toMatchObject({ + serviceTier: "default", }); } finally { await session.close(); @@ -165,8 +362,8 @@ describe("Codex app-server provider features", () => { try { expect(session.features).toContainEqual( expect.objectContaining({ - id: "fast_mode", - value: true, + id: "service_tier", + value: "priority", }), ); await session.setModel("gpt-5.6-sol"); @@ -174,26 +371,18 @@ describe("Codex app-server provider features", () => { await session.startTurn("hello"); await expect(appServer.waitForTurnStart()).resolves.toMatchObject({ model: "gpt-6-astra", - serviceTier: "fast", + serviceTier: "priority", }); } finally { await session.close(); } }); - test("features returns fast and plan toggles when supported", async () => { + test("features returns speed selector and plan toggle when supported", async () => { const { session } = await createConnectedSession(); expect(session.features).toEqual([ - { - type: "toggle", - id: "fast_mode", - label: "Fast", - description: "Priority inference at increased usage", - tooltip: "Toggle fast mode", - icon: "zap", - value: false, - }, + speedFeature("default"), { type: "toggle", id: "plan_mode", @@ -205,19 +394,11 @@ describe("Codex app-server provider features", () => { }, ]); - await session.setFeature?.("fast_mode", true); + await session.setFeature?.("service_tier", "priority"); await session.setFeature?.("plan_mode", true); expect(session.features).toEqual([ - { - type: "toggle", - id: "fast_mode", - label: "Fast", - description: "Priority inference at increased usage", - tooltip: "Toggle fast mode", - icon: "zap", - value: true, - }, + speedFeature("priority"), { type: "toggle", id: "plan_mode", @@ -265,23 +446,23 @@ describe("Codex app-server provider features", () => { ]); await session.startTurn("hello"); - await expect(appServer.waitForTurnStart()).resolves.not.toMatchObject({ - serviceTier: expect.anything(), + await expect(appServer.waitForTurnStart()).resolves.toMatchObject({ + serviceTier: "default", }); }); - test("setFeature('fast_mode', true) sets serviceTier to fast", async () => { + test("selecting Fast sends the catalog service tier", async () => { const { session, appServer } = await createConnectedSession(); - await session.setFeature?.("fast_mode", true); + await session.setFeature?.("service_tier", "priority"); await session.startTurn("hello"); await expect(appServer.waitForTurnStart()).resolves.toMatchObject({ - serviceTier: "fast", + serviceTier: "priority", }); }); - test("setFeature('fast_mode', false) clears serviceTier to null", async () => { + test("legacy Fast off explicitly selects Normal", async () => { const { session, appServer } = await createConnectedSession({ featureValues: { fast_mode: true }, }); @@ -289,8 +470,8 @@ describe("Codex app-server provider features", () => { await session.setFeature?.("fast_mode", false); await session.startTurn("hello"); - await expect(appServer.waitForTurnStart()).resolves.not.toMatchObject({ - serviceTier: expect.anything(), + await expect(appServer.waitForTurnStart()).resolves.toMatchObject({ + serviceTier: "default", }); }); @@ -330,15 +511,7 @@ describe("Codex app-server provider features", () => { }); expect(session.features).toEqual([ - { - type: "toggle", - id: "fast_mode", - label: "Fast", - description: "Priority inference at increased usage", - tooltip: "Toggle fast mode", - icon: "zap", - value: true, - }, + speedFeature("priority"), { type: "toggle", id: "plan_mode", @@ -352,7 +525,7 @@ describe("Codex app-server provider features", () => { await session.startTurn("hello"); await expect(appServer.waitForTurnStart()).resolves.toMatchObject({ - serviceTier: "fast", + serviceTier: "priority", collaborationMode: expect.objectContaining({ mode: "plan", }), @@ -362,11 +535,11 @@ describe("Codex app-server provider features", () => { test("startTurn includes serviceTier when fast mode is enabled", async () => { const { session, appServer } = await createConnectedSession(); - await session.setFeature?.("fast_mode", true); + await session.setFeature?.("service_tier", "priority"); await session.startTurn("hello"); await expect(appServer.waitForTurnStart()).resolves.toMatchObject({ - serviceTier: "fast", + serviceTier: "priority", }); }); @@ -388,7 +561,7 @@ describe("Codex app-server provider features", () => { msg: "Starting Codex app-server turn", model: "gpt-5.4", modeId: "auto", - serviceTier: "fast", + serviceTier: "priority", cwd: "/tmp/codex-fast-mode-test", }); expect(JSON.stringify(entry)).not.toContain(prompt); @@ -397,7 +570,7 @@ describe("Codex app-server provider features", () => { test("setModel clears fast mode when switching to an unsupported model", async () => { const { session, appServer } = await createConnectedSession(); - await session.setFeature?.("fast_mode", true); + await session.setFeature?.("service_tier", "priority"); await session.setModel("gpt-3.5-turbo"); expect(session.features).toEqual([ @@ -413,8 +586,8 @@ describe("Codex app-server provider features", () => { ]); await session.startTurn("hello"); - await expect(appServer.waitForTurnStart()).resolves.not.toMatchObject({ - serviceTier: expect.anything(), + await expect(appServer.waitForTurnStart()).resolves.toMatchObject({ + serviceTier: "default", }); }); diff --git a/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts b/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts index d55a0b2a79..f5fb059255 100644 --- a/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts +++ b/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts @@ -804,6 +804,7 @@ let buffer = ""; function resultFor(method, params) { if (method === "initialize") return {}; if (method === "collaborationMode/list") return { data: [] }; + if (method === "model/list") return { data: [] }; if (method === "skills/list") { const cwds = params && params.cwds; const projectCwd = ${JSON.stringify(projectCwd)}; @@ -2229,7 +2230,12 @@ describe("Codex app-server provider", () => { throw new Error(`resumeSession timed out; thread requests: ${threadRequests.join(", ")}`); } - expect(threadRequests).toEqual(["config/read", "thread/loaded/list", "thread/resume"]); + expect(threadRequests).toEqual([ + "model/list", + "config/read", + "thread/loaded/list", + "thread/resume", + ]); expect(outcome).toBe("rejected"); appServer.assertNoErrors(); }); diff --git a/packages/server/src/server/agent/providers/codex-app-server-agent.ts b/packages/server/src/server/agent/providers/codex-app-server-agent.ts index f86d5de653..1fbe22ec30 100644 --- a/packages/server/src/server/agent/providers/codex-app-server-agent.ts +++ b/packages/server/src/server/agent/providers/codex-app-server-agent.ts @@ -73,7 +73,12 @@ import { import { createPathEquivalenceMatcher } from "../../../utils/path.js"; import { spawnProcess } from "../../../utils/spawn.js"; import { extractCodexTerminalSessionId, nonEmptyString } from "./tool-call-mapper-utils.js"; -import { buildCodexFeatures, codexModelSupportsFastMode } from "./codex-feature-definitions.js"; +import { + buildCodexFeatures, + CodexServiceTierSchema, + readCodexServiceTier, + type CodexServiceTier, +} from "./codex-feature-definitions.js"; import { CodexAppServerClient, CodexAppServerRpcError, @@ -929,6 +934,7 @@ interface CodexModel { model?: string; defaultReasoningEffort?: string; supportedReasoningEfforts?: CodexReasoningEffortEntry[]; + serviceTiers?: CodexServiceTier[]; } const CodexModelListResponseSchema = z.object({ @@ -936,6 +942,7 @@ const CodexModelListResponseSchema = z.object({ .array( z.object({ id: z.string(), + serviceTiers: z.array(CodexServiceTierSchema).optional(), displayName: z.string().optional(), description: z.string().optional(), isDefault: z.boolean().optional(), @@ -3426,7 +3433,8 @@ export class CodexAppServerAgentSession implements AgentSession { private activeForegroundTurnId: string | null = null; private activeClientMessageId: string | null = null; private cachedRuntimeInfo: AgentRuntimeInfo | null = null; - private serviceTier: "fast" | null = null; + private serviceTier: string | null = null; + private speedModels: CodexModel[] = []; private planModeEnabled = false; private historyPending = false; private persistedHistory: PersistedTimelineEntry[] = []; @@ -3517,9 +3525,7 @@ export class CodexAppServerAgentSession implements AgentSession { this.asyncQuestions = new CodexAsyncQuestions(resumeHandle?.metadata?.asyncQuestions); this.codexHome = deps.codexHome ?? resolveCodexHomeDir(process.env); this.config.thinkingOptionId = normalizeCodexThinkingOptionId(this.config.thinkingOptionId); - if (this.config.featureValues?.fast_mode && codexModelSupportsFastMode(this.config.model)) { - this.serviceTier = "fast"; - } + this.serviceTier = readCodexServiceTier(this.config.featureValues); if (this.config.featureValues?.plan_mode) { this.planModeEnabled = true; } @@ -3547,8 +3553,8 @@ export class CodexAppServerAgentSession implements AgentSession { get features(): AgentFeature[] { return buildCodexFeatures({ - modelId: this.config.model, - fastModeEnabled: this.serviceTier === "fast", + serviceTiers: this.currentServiceTiers(), + serviceTier: this.serviceTier ?? "default", planModeEnabled: this.planModeEnabled, planModeAvailable: this.hasPlanCollaborationMode(), }); @@ -3604,6 +3610,9 @@ export class CodexAppServerAgentSession implements AgentSession { await client.request("initialize", buildCodexAppServerInitializeParams()); client.notify("initialized", {}); + this.speedModels = + CodexModelListResponseSchema.parse(await client.request("model/list", {})).data ?? []; + this.reconcileServiceTier(); await this.loadResolvedWorkspaceWrite(); await this.loadCollaborationModes(); await this.loadSkills(); @@ -3808,18 +3817,30 @@ export class CodexAppServerAgentSession implements AgentSession { this.resolvedCollaborationMode = this.resolveCollaborationMode(); } - private applyFeatureValue(featureId: "fast_mode" | "plan_mode", value: boolean): void { - this.config.featureValues = { - ...this.config.featureValues, - [featureId]: value, - }; + private currentServiceTiers(): CodexServiceTier[] { + const selectedModel = this.config.model + ? this.speedModels.find( + (model) => model.id === this.config.model || model.model === this.config.model, + ) + : (this.speedModels.find((model) => model.isDefault) ?? this.speedModels[0]); + return selectedModel?.serviceTiers ?? []; + } - if (featureId === "fast_mode") { - this.serviceTier = value ? "fast" : null; - this.cachedRuntimeInfo = null; - return; + private reconcileServiceTier(): void { + const tiers = this.currentServiceTiers(); + // COMPAT(codexFastPreference): added in v0.10.0, remove after 2027-03-29 once saved Fast preferences have migrated. + if (this.serviceTier === "fast" && !tiers.some((tier) => tier.id === "fast")) { + this.serviceTier = tiers.find((tier) => tier.name.toLowerCase() === "fast")?.id ?? null; } + if (this.serviceTier !== "default" && !tiers.some((tier) => tier.id === this.serviceTier)) { + this.serviceTier = null; + } + const { fast_mode: _legacyFast, ...values } = this.config.featureValues ?? {}; + this.config.featureValues = { ...values, service_tier: this.serviceTier ?? "default" }; + } + private applyFeatureValue(featureId: "plan_mode", value: boolean): void { + this.config.featureValues = { ...this.config.featureValues, [featureId]: value }; this.planModeEnabled = value; this.refreshResolvedCollaborationMode(); this.cachedRuntimeInfo = null; @@ -4150,9 +4171,7 @@ export class CodexAppServerAgentSession implements AgentSession { if (thinkingOptionId) { params.effort = thinkingOptionId; } - if (this.serviceTier) { - params.serviceTier = this.serviceTier; - } + params.serviceTier = this.serviceTier ?? "default"; if (this.resolvedCollaborationMode) { params.collaborationMode = { mode: this.resolvedCollaborationMode.mode, @@ -4570,9 +4589,7 @@ export class CodexAppServerAgentSession implements AgentSession { async setModel(modelId: string | null): Promise { this.config.model = modelId ?? undefined; - if (!codexModelSupportsFastMode(this.config.model)) { - this.serviceTier = null; - } + this.reconcileServiceTier(); this.refreshResolvedCollaborationMode(); this.cachedRuntimeInfo = null; } @@ -4587,13 +4604,27 @@ export class CodexAppServerAgentSession implements AgentSession { } async setFeature(featureId: string, value: unknown): Promise { + // COMPAT(codexFastPreference): added in v0.10.0, remove after 2027-03-29 once clients use service_tier. if (featureId === "fast_mode") { - if (Boolean(value) && !codexModelSupportsFastMode(this.config.model)) { + const fast = this.currentServiceTiers().find((tier) => tier.name.toLowerCase() === "fast"); + if (value && !fast) throw new Error( `Codex fast mode is not available for model '${this.config.model ?? "default"}'`, ); + return this.setFeature("service_tier", value ? fast!.id : "default"); + } + if (featureId === "service_tier") { + if ( + typeof value !== "string" || + (value !== "default" && !this.currentServiceTiers().some((tier) => tier.id === value)) + ) { + throw new Error( + `Codex speed '${String(value)}' is not available for model '${this.config.model ?? "default"}'`, + ); } - this.applyFeatureValue("fast_mode", Boolean(value)); + this.serviceTier = value; + this.reconcileServiceTier(); + this.cachedRuntimeInfo = null; return; } if (featureId === "plan_mode") { @@ -5214,6 +5245,7 @@ export class CodexAppServerAgentSession implements AgentSession { private async applyDefaultModelAndThinking(): Promise { const { model, thinkingOptionId } = await this.resolveModelAndThinking(); this.config.model = model; + this.reconcileServiceTier(); this.config.thinkingOptionId = thinkingOptionId; return model; } diff --git a/packages/server/src/server/agent/providers/codex-feature-definitions.ts b/packages/server/src/server/agent/providers/codex-feature-definitions.ts index d2588a5a5f..5eb11d8a0c 100644 --- a/packages/server/src/server/agent/providers/codex-feature-definitions.ts +++ b/packages/server/src/server/agent/providers/codex-feature-definitions.ts @@ -1,27 +1,33 @@ import type { AgentFeature, AgentFeatureToggle } from "../agent-sdk-types.js"; -// Codex Fast is distinct from API Priority processing. Keep model support aligned with -// https://developers.openai.com/codex/speed and https://developers.openai.com/codex/models. -const CODEX_FAST_MODE_SUPPORTED_MODELS = new Set([ - "gpt-6-astra", - "gpt-6-sol", - "gpt-6-luna", - "gpt-5.6", - "gpt-5.6-sol", - "gpt-5.6-terra", - "gpt-5.6-luna", - "gpt-5.5", - "gpt-5.4", -]); +import { z } from "zod"; -export const CODEX_FAST_MODE_FEATURE: Omit = { - type: "toggle", - id: "fast_mode", - label: "Fast", - description: "Priority inference at increased usage", - tooltip: "Toggle fast mode", - icon: "zap", -}; +export const CodexServiceTierSchema = z.object({ + id: z.string(), + name: z.string(), + description: z.string(), +}); +export type CodexServiceTier = z.infer; + +function buildCodexSpeedFeature(tiers: CodexServiceTier[], value: string): AgentFeature[] { + if (tiers.length === 0) return []; + return [ + { + type: "select", + id: "service_tier", + label: "Speed", + description: "Choose processing speed. Faster tiers increase usage.", + tooltip: "Select speed", + icon: "zap", + desktopTrigger: "icon", + value, + options: [ + { id: "default", label: "Normal", isDefault: true }, + ...tiers.map((tier) => ({ id: tier.id, label: tier.name })), + ], + }, + ]; +} export const CODEX_PLAN_MODE_FEATURE: Omit = { type: "toggle", @@ -32,40 +38,22 @@ export const CODEX_PLAN_MODE_FEATURE: Omit = { icon: "list-todo", }; -function normalizeCodexModelId(modelId: string | null | undefined): string | null { - const normalized = typeof modelId === "string" ? modelId.trim() : ""; - return normalized.length > 0 ? normalized : null; -} - -export function codexModelSupportsFastMode(modelId: string | null | undefined): boolean { - const normalizedModelId = normalizeCodexModelId(modelId); - if (!normalizedModelId) { - return false; - } - return CODEX_FAST_MODE_SUPPORTED_MODELS.has(normalizedModelId); -} - export function buildCodexFeatures(input: { - modelId: string | null | undefined; - fastModeEnabled: boolean; + serviceTiers: CodexServiceTier[]; + serviceTier: string; planModeEnabled: boolean; planModeAvailable?: boolean; }): AgentFeature[] { - const features: AgentFeature[] = []; - - if (codexModelSupportsFastMode(input.modelId)) { - features.push({ - ...CODEX_FAST_MODE_FEATURE, - value: input.fastModeEnabled, - }); - } - + const features = buildCodexSpeedFeature(input.serviceTiers, input.serviceTier); if (input.planModeAvailable !== false) { - features.push({ - ...CODEX_PLAN_MODE_FEATURE, - value: input.planModeEnabled, - }); + features.push({ ...CODEX_PLAN_MODE_FEATURE, value: input.planModeEnabled }); } - return features; } + +export function readCodexServiceTier(values: Record | undefined): string | null { + const tier = values?.service_tier; + if (typeof tier === "string") return tier; + // COMPAT(codexFastPreference): added in v0.10.0, remove after 2027-03-29 once saved Fast preferences have migrated. + return values?.fast_mode ? "fast" : null; +} From d3c902e9fab97e451dc52903333a46299eb13fad Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Tue, 29 Sep 2026 22:10:27 +0200 Subject: [PATCH 007/100] fix(opencode): fill the context meter during v2 turns (#5710) OpenCode v2 sessions never reported context used or the model's context limit, so the composer meter was an empty ring during a turn and vanished when it ended. Each finished step now reports its context against the selected model's limit. Closes #5701 Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> Co-authored-by: Matt Cowger --- .../agent/providers/opencode/v2/session.ts | 14 +++ .../agent/providers/opencode/v2/usage.test.ts | 100 ++++++++++++++++++ .../agent/providers/opencode/v2/usage.ts | 61 +++++++++++ 3 files changed, 175 insertions(+) create mode 100644 packages/server/src/server/agent/providers/opencode/v2/usage.test.ts create mode 100644 packages/server/src/server/agent/providers/opencode/v2/usage.ts diff --git a/packages/server/src/server/agent/providers/opencode/v2/session.ts b/packages/server/src/server/agent/providers/opencode/v2/session.ts index f7a19006d9..cb71ff7212 100644 --- a/packages/server/src/server/agent/providers/opencode/v2/session.ts +++ b/packages/server/src/server/agent/providers/opencode/v2/session.ts @@ -35,6 +35,7 @@ import { features } from "./configuration.js"; import { commands } from "./commands.js"; import { messages } from "./history.js"; import { SessionPermissions } from "./permissions.js"; +import { SessionUsage } from "./usage.js"; export class OpenCodeV2Session implements AgentSession { readonly provider = "opencode"; @@ -45,6 +46,7 @@ export class OpenCodeV2Session implements AgentSession { private readonly permissions: SessionPermissions; private readonly turns: SessionTurns; private readonly children: SessionChildren; + private readonly usage: SessionUsage; private stream: Promise | null = null; private streamAbort = new AbortController(); private exited = false; @@ -101,6 +103,17 @@ export class OpenCodeV2Session implements AgentSession { await this.permissions.respondToPermission(request.id, { behavior: "deny" }); }, }); + this.usage = new SessionUsage({ + client: () => this.client, + cwd: config.cwd, + info: () => this.info, + emit: (event) => this.emit(event), + reportError: (error) => + this.logger.warn( + { error: toDiagnosticErrorMessage(error) }, + "OpenCode context usage update failed", + ), + }); } get id() { return this.info.id; @@ -465,6 +478,7 @@ export class OpenCodeV2Session implements AgentSession { return; } this.permissions.observe(event); + this.usage.observe(event); this.turns.observe(event); this.scheduleReconcile(); } diff --git a/packages/server/src/server/agent/providers/opencode/v2/usage.test.ts b/packages/server/src/server/agent/providers/opencode/v2/usage.test.ts new file mode 100644 index 0000000000..4fa64079c2 --- /dev/null +++ b/packages/server/src/server/agent/providers/opencode/v2/usage.test.ts @@ -0,0 +1,100 @@ +import type { ModelInfo, TokenUsageInfo } from "@opencode/client"; +import { describe, expect, test } from "vitest"; +import { OpenCodeV2AgentClient } from "./agent.js"; +import { V2Harness } from "../test-utils/v2-harness.js"; +import { createTestLogger } from "../../../../../test-utils/test-logger.js"; +import type { AgentUsage } from "../../../agent-sdk-types.js"; + +function model(context: number): ModelInfo { + return { + id: "model", + modelID: "model", + providerID: "test", + name: "Model", + capabilities: { tools: true, input: ["text"], output: ["text"] }, + variants: [], + time: { released: 1 }, + cost: [], + status: "active", + enabled: true, + limit: { context, output: 10_000 }, + }; +} + +function stepEnded(harness: V2Harness, id: string, tokens: TokenUsageInfo) { + harness.push({ + id, + created: 2, + type: "session.step.ended", + durable: { aggregateID: "session", seq: 1, version: 1 }, + data: { + sessionID: "session", + assistantMessageID: "answer", + finish: "tool-calls", + cost: 0.1, + tokens, + }, + }); +} + +describe("OpenCode v2 context usage", () => { + test("reports each step's context against the model limit, in step order", async () => { + const harness = new V2Harness(); + harness.info.model = { providerID: "test", id: "model" }; + harness.info.tokens = { + input: 900, + output: 90, + reasoning: 0, + cache: { read: 40, write: 0 }, + }; + harness.info.cost = 0.5; + // Each lookup answers faster than the one before it, so a lookup per step would + // deliver step 1's usage after step 2's. + const lookupDelays = [50, 0]; + harness.api.model.list = async (input) => { + await new Promise((resolve) => setTimeout(resolve, lookupDelays.shift() ?? 0)); + return { + location: { directory: input.location.directory }, + data: [model(200_000)], + }; + }; + const client = new OpenCodeV2AgentClient({ + logger: createTestLogger(), + runtime: harness.runtime, + }); + const session = await client.createSession({ + provider: "opencode", + cwd: "/tmp/project", + }); + const usage: AgentUsage[] = []; + session.subscribe((event) => { + if (event.type === "usage_updated") usage.push(event.usage); + }); + try { + stepEnded(harness, "step-1", { + input: 100, + output: 10, + reasoning: 5, + cache: { read: 1_000, write: 20 }, + }); + stepEnded(harness, "step-2", { + input: 300, + output: 10, + reasoning: 5, + cache: { read: 1_000, write: 20 }, + }); + await expect.poll(() => usage.length).toBe(2); + expect(usage.map((entry) => entry.contextWindowUsedTokens)).toEqual([1_135, 1_335]); + expect(usage[1]).toEqual({ + inputTokens: 900, + outputTokens: 90, + cachedInputTokens: 40, + totalCostUsd: 0.5, + contextWindowUsedTokens: 1_335, + contextWindowMaxTokens: 200_000, + }); + } finally { + await session.close(); + } + }); +}); diff --git a/packages/server/src/server/agent/providers/opencode/v2/usage.ts b/packages/server/src/server/agent/providers/opencode/v2/usage.ts new file mode 100644 index 0000000000..17a4885cbc --- /dev/null +++ b/packages/server/src/server/agent/providers/opencode/v2/usage.ts @@ -0,0 +1,61 @@ +import type { ModelRef, OpenCodeEvent, SessionInfo, TokenUsageInfo } from "@opencode/client"; +import type { AgentStreamEvent } from "../../../agent-sdk-types.js"; +import type { V2Api } from "./api.js"; +import { usageFromV2 } from "./mapping.js"; + +interface UsageOptions { + client(): V2Api; + cwd: string; + info(): SessionInfo; + emit(event: AgentStreamEvent): void; + reportError(error: unknown): void; +} + +// A step is one model call, so its tokens are the context that call used. +function contextWindowUsedTokens(tokens: TokenUsageInfo) { + return tokens.input + tokens.output + tokens.reasoning + tokens.cache.read + tokens.cache.write; +} + +export class SessionUsage { + private readonly limits = new Map>(); + private reports: Promise = Promise.resolve(); + constructor(private readonly options: UsageOptions) {} + observe(event: OpenCodeEvent) { + if (event.type !== "session.step.ended") return; + const { tokens } = event.data; + // Reports stay in step order while a model's context limit is still being looked up. + this.reports = this.reports + .then(() => this.report(tokens)) + .catch((error: unknown) => this.options.reportError(error)); + } + private async report(tokens: TokenUsageInfo) { + const model = this.options.info().model; + const contextWindowMaxTokens = model ? await this.contextLimit(model) : undefined; + this.options.emit({ + type: "usage_updated", + provider: "opencode", + usage: { + ...usageFromV2(this.options.info()), + contextWindowUsedTokens: contextWindowUsedTokens(tokens), + ...(contextWindowMaxTokens === undefined ? {} : { contextWindowMaxTokens }), + }, + }); + } + private contextLimit(model: ModelRef) { + const key = `${model.providerID}/${model.id}`; + const cached = this.limits.get(key); + if (cached) return cached; + const limit = this.options + .client() + .model.list({ location: { directory: this.options.cwd } }) + .then( + (catalog) => + catalog.data.find( + (entry) => entry.providerID === model.providerID && entry.id === model.id, + )?.limit.context, + ); + this.limits.set(key, limit); + limit.catch(() => this.limits.delete(key)); + return limit; + } +} From 767b0e970b50e3a3dfccbfbc75bac5e86d5cea7f Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Tue, 29 Sep 2026 22:59:00 +0200 Subject: [PATCH 008/100] Rewind legacy Codex threads on Codex versions without thread/rollback (#5711) Codex 0.156 removed thread/rollback. Rewinding a legacy-history thread forked it and then called rollback on the fork, so the rewind failed and left an unused fork behind. Read the Codex version from the initialize userAgent and, when rollback is gone, fork before the target turn the same way paginated threads already do. Older servers keep fork plus rollback. Closes #5616 Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> Co-authored-by: Bae Seongmin <83870598+3ae3ae@users.noreply.github.com> --- .../providers/codex-app-server-agent.test.ts | 60 +++++++++++++++++++ .../agent/providers/codex-app-server-agent.ts | 19 +++++- .../agent/providers/codex/rewind.test.ts | 5 ++ .../server/agent/providers/codex/rewind.ts | 10 +++- 4 files changed, 90 insertions(+), 4 deletions(-) diff --git a/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts b/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts index f5fb059255..e0e7fa0379 100644 --- a/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts +++ b/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts @@ -1965,6 +1965,66 @@ describe("Codex app-server provider", () => { await session.close(); }); + test("rewinds a legacy conversation with a bounded fork on Codex without thread/rollback", async () => { + const appServer = createFakeCodexAppServer({ + initialize: () => ({ userAgent: "paseo/0.159.0 (Ubuntu 26.4.0; x86_64) (paseo; 0)" }), + "thread/read": () => ({ + thread: { id: "thread-1", historyMode: "legacy", turns: [] }, + }), + "thread/rollback": () => ({ + __jsonRpcError: { + code: -32600, + message: "Invalid request: unknown variant `thread/rollback`", + }, + }), + }); + const session = new CodexAppServerAgentSession( + createConfig({ cwd: "/workspace/project" }), + null, + createTestLogger(), + async () => appServer.child, + ); + + await session.startTurn("remember first"); + emitCodexUserMessage(appServer, { + id: "codex-first", + text: "remember first", + turnId: "turn-first", + }); + appServer.completeTurn(); + await session.startTurn("remember second"); + emitCodexUserMessage(appServer, { + id: "codex-second", + text: "remember second", + turnId: "turn-second", + }); + appServer.completeTurn(); + + await session.revertConversation({ messageId: "codex-second" }); + + const forkRequests = appServer + .requests() + .filter((request) => request.method === "thread/fork") + .map((request) => request.params); + expect(forkRequests).toEqual([ + { + threadId: "thread-1", + beforeTurnId: "turn-second", + cwd: "/workspace/project", + model: "gpt-5.4", + serviceTier: null, + excludeTurns: false, + persistExtendedHistory: true, + }, + ]); + expect(appServer.recordedRollbacks).toEqual([]); + await expect(session.getRuntimeInfo()).resolves.toMatchObject({ + sessionId: "forked-thread", + }); + appServer.assertNoErrors(); + await session.close(); + }); + test("rewinds a paginated conversation through the public session capability", async () => { const appServer = createFakeCodexAppServer({ "thread/read": () => ({ diff --git a/packages/server/src/server/agent/providers/codex-app-server-agent.ts b/packages/server/src/server/agent/providers/codex-app-server-agent.ts index 1fbe22ec30..cee50fbf4c 100644 --- a/packages/server/src/server/agent/providers/codex-app-server-agent.ts +++ b/packages/server/src/server/agent/providers/codex-app-server-agent.ts @@ -174,6 +174,9 @@ const CODEX_PLAN_IMPLEMENTATION_PROMPT_PREFIX = // (and the /goal slash command) when the binary is too old. const CODEX_GOALS_MIN_VERSION: readonly [number, number, number] = [0, 128, 0]; const CODEX_AUTO_REVIEW_MIN_VERSION: readonly [number, number, number] = [0, 115, 0]; +// Codex removed `thread/rollback` in 0.156.0, so rewinding a legacy thread there +// has to fork before the target turn instead. +const CODEX_THREAD_ROLLBACK_REMOVED_VERSION: readonly [number, number, number] = [0, 156, 0]; function parseCodexVersion(versionOutput: string): [number, number, number] | null { const match = versionOutput.match(/(\d+)\.(\d+)\.(\d+)/); @@ -194,6 +197,15 @@ function codexVersionAtLeast( return true; } +// The initialize userAgent starts with `/`. An agent we +// cannot parse is treated as an older server that still has rollback. +function codexServerHasThreadRollback(userAgent: unknown): boolean { + return ( + typeof userAgent !== "string" || + !codexVersionAtLeast(userAgent, CODEX_THREAD_ROLLBACK_REMOVED_VERSION) + ); +} + type GoalSubcommand = | { kind: "set"; objective: string } | { kind: "pause" } @@ -3428,6 +3440,7 @@ export class CodexAppServerAgentSession implements AgentSession { cancelRequested: boolean; } | null = null; private client: CodexAppServerClient | null = null; + private threadRollbackAvailable = true; private readonly subscribers = new Set<(event: AgentStreamEvent) => void>(); private nextTurnOrdinal = 0; private activeForegroundTurnId: string | null = null; @@ -3607,7 +3620,10 @@ export class CodexAppServerAgentSession implements AgentSession { this.registerRequestHandlers(); try { - await client.request("initialize", buildCodexAppServerInitializeParams()); + const initialized = toObjectRecord( + await client.request("initialize", buildCodexAppServerInitializeParams()), + ); + this.threadRollbackAvailable = codexServerHasThreadRollback(initialized?.userAgent); client.notify("initialized", {}); this.speedModels = @@ -4927,6 +4943,7 @@ export class CodexAppServerAgentSession implements AgentSession { serviceTier: this.serviceTier, config: this.buildCodexInnerConfig(), userMessageTurns: this.codexUserMessageTurns(), + threadRollbackAvailable: this.threadRollbackAvailable, setThreadId: async (threadId) => { this.currentThreadId = threadId; this.cachedRuntimeInfo = null; diff --git a/packages/server/src/server/agent/providers/codex/rewind.test.ts b/packages/server/src/server/agent/providers/codex/rewind.test.ts index 923b8074bb..da7fe6a8f5 100644 --- a/packages/server/src/server/agent/providers/codex/rewind.test.ts +++ b/packages/server/src/server/agent/providers/codex/rewind.test.ts @@ -96,6 +96,7 @@ describe("Codex Rewind", () => { model: "gpt-5.4-mini", serviceTier: null, userMessageTurns, + threadRollbackAvailable: true, setThreadId: (threadId) => { reboundThreadId = threadId; }, @@ -131,6 +132,7 @@ describe("Codex Rewind", () => { threadId: "source-thread", messageId: "codex-second", userMessageTurns, + threadRollbackAvailable: true, setThreadId: (threadId) => { reboundThreadId = threadId; }, @@ -175,6 +177,7 @@ describe("Codex Rewind", () => { model: "gpt-5.4-mini", serviceTier: null, userMessageTurns, + threadRollbackAvailable: true, setThreadId: (threadId) => { reboundThreadId = threadId; }, @@ -218,6 +221,7 @@ describe("Codex Rewind", () => { model: "gpt-5.4-mini", serviceTier: null, userMessageTurns, + threadRollbackAvailable: true, setThreadId: (threadId) => { reboundThreadId = threadId; }, @@ -239,6 +243,7 @@ describe("Codex Rewind", () => { threadId: "source-thread", messageId: "missing-message", userMessageTurns, + threadRollbackAvailable: true, setThreadId: () => undefined, }), ).rejects.toThrow("Codex could not find user message missing-message"); diff --git a/packages/server/src/server/agent/providers/codex/rewind.ts b/packages/server/src/server/agent/providers/codex/rewind.ts index 1c3d3c471c..021d57645c 100644 --- a/packages/server/src/server/agent/providers/codex/rewind.ts +++ b/packages/server/src/server/agent/providers/codex/rewind.ts @@ -69,6 +69,7 @@ export async function revertCodexConversation(input: { serviceTier?: string | null; config?: Record | null; userMessageTurns: CodexUserMessageTurnIndex; + threadRollbackAvailable: boolean; setThreadId: (threadId: string) => void | Promise; }): Promise { if (!input.threadId) { @@ -98,8 +99,10 @@ export async function revertCodexConversation(input: { persistExtendedHistory: true, }; - const historyMode = await readCodexThreadHistoryMode(input.client, input.threadId); - if (historyMode === "paginated") { + if ( + !input.threadRollbackAvailable || + (await readCodexThreadHistoryMode(input.client, input.threadId)) === "paginated" + ) { if (!targetTurn.turnId) { throw new Error(`Codex could not find the turn containing user message ${input.messageId}`); } @@ -111,7 +114,8 @@ export async function revertCodexConversation(input: { return; } - // Fork is non-destructive: the old thread file stays on disk and remains + // Legacy threads on Codex before 0.156 fork and then roll back. Fork is + // non-destructive: the old thread file stays on disk and remains // recoverable with `codex resume ` if the rewind target was wrong. const forked = await forkCodexThread(input.client, forkParams); const forkedThreadId = forked.thread.id; From 53ee9cd9930d9479af318c1ed29713a0bfb5f47b Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Wed, 30 Sep 2026 00:06:21 +0200 Subject: [PATCH 009/100] fix(codex): settle declined commands and file edits as failed (#5717) Codex completes a command or file change with status "declined" when its approval is denied. The shared status normalizer did not know that word and fell back to "running", so the denied command kept a spinner after the turn ended. Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> Co-authored-by: Bae Seongmin <83870598+3ae3ae@users.noreply.github.com> --- .../providers/codex/tool-call-mapper.test.ts | 23 +++++++++++++++++++ .../agent/providers/tool-call-mapper-utils.ts | 1 + 2 files changed, 24 insertions(+) diff --git a/packages/server/src/server/agent/providers/codex/tool-call-mapper.test.ts b/packages/server/src/server/agent/providers/codex/tool-call-mapper.test.ts index 6ce2d4fe13..566fd10083 100644 --- a/packages/server/src/server/agent/providers/codex/tool-call-mapper.test.ts +++ b/packages/server/src/server/agent/providers/codex/tool-call-mapper.test.ts @@ -656,6 +656,29 @@ describe("codex tool-call mapper", () => { }); }); + it.each([ + { + type: "commandExecution", + id: "codex-declined-command", + status: "declined", + command: "printf ok > /outside/permission.txt", + cwd: "/tmp/repo", + aggregatedOutput: null, + exitCode: null, + }, + { + type: "fileChange", + id: "codex-declined-patch", + status: "declined", + changes: [], + }, + ])("settles a declined $type as failed instead of running", (threadItem) => { + const item = expectMapped(mapCodexToolCallFromThreadItem(threadItem)); + + expect(item.callId).toBe(threadItem.id); + expect(item.status).toBe("failed"); + }); + it("maps unknown tools to unknown detail with raw payloads", () => { const item = expectMapped( mapCodexToolCallEnvelope({ diff --git a/packages/server/src/server/agent/providers/tool-call-mapper-utils.ts b/packages/server/src/server/agent/providers/tool-call-mapper-utils.ts index 281a2bcf32..cd0e4776b7 100644 --- a/packages/server/src/server/agent/providers/tool-call-mapper-utils.ts +++ b/packages/server/src/server/agent/providers/tool-call-mapper-utils.ts @@ -7,6 +7,7 @@ const FAILED_STATUS_VOCAB = new Set([ "errored", "rejected", "denied", + "declined", ]); const CANCELED_STATUS_VOCAB = new Set(["canceled", "cancelled", "interrupted", "aborted"]); const COMPLETED_STATUS_VOCAB = new Set(["completed", "complete", "done", "success", "succeeded"]); From 3fea1283828950f679c9da2c880830c73a6bb9b3 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Wed, 30 Sep 2026 01:09:50 +0200 Subject: [PATCH 010/100] docs(changelog): add 0.10.2 release notes (cherry picked from commit c5b4b7bae3cc79ba45ccf89343c7ec5edafb772d) --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 548a24aaed..68bf52ca78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## 0.10.2 - 2026-09-30 + +### Fixed + +- Fixed OpenCode v2 turns longer than five minutes failing with `UND_ERR_HEADERS_TIMEOUT` ([#5674](https://github.com/getpaseo/paseo/pull/5674)) +- Fixed the context meter staying empty during OpenCode v2 turns and disappearing after them ([#5710](https://github.com/getpaseo/paseo/pull/5710) by [@mcowger](https://github.com/mcowger)) +- Fixed OpenCode v2 question cards showing only the header and offering no typed answer ([#5679](https://github.com/getpaseo/paseo/pull/5679)) +- Fixed OpenCode v2 patch edits from GPT models showing as a raw Patch card instead of an edit diff ([#5696](https://github.com/getpaseo/paseo/pull/5696)) +- Fixed completed OpenCode v2 edits showing no diff ([#5609](https://github.com/getpaseo/paseo/pull/5609)) + ## 0.10.1 - 2026-09-29 ### Added From 4e9a4582b7f607b91889b388f86cb42ecc6896da Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Wed, 30 Sep 2026 02:12:03 +0200 Subject: [PATCH 011/100] fix(app): stop connecting to a host removed during its first probe (#5722) Closes #4815 Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> --- packages/app/src/runtime/host-runtime.test.ts | 32 +++++++++++++++++++ packages/app/src/runtime/host-runtime.ts | 7 ++++ 2 files changed, 39 insertions(+) diff --git a/packages/app/src/runtime/host-runtime.test.ts b/packages/app/src/runtime/host-runtime.test.ts index 280eae793e..fbf7764f31 100644 --- a/packages/app/src/runtime/host-runtime.test.ts +++ b/packages/app/src/runtime/host-runtime.test.ts @@ -4017,6 +4017,38 @@ describe("HostRuntimeStore initial connection hint bootstrap", () => { expect(store.getSnapshot("srv_deep")?.connectionStatus).toBe("error"); store.syncHosts([]); }); + it("stops connecting to a host removed while its first probe is pending", async () => { + useHostRuntimeClock(); + const firstProbe = createDeferred(); + let connectCalls = 0; + const store = new HostRuntimeStore({ + storage: createMemoryHostRuntimeStorage(), + deps: { + createClient: () => new FakeDaemonClient() as unknown as DaemonClient, + connectToDaemon: async ({ host }) => { + connectCalls += 1; + if (connectCalls === 1) await firstProbe.promise; + return { + client: makeConnectedProbeClient(5) as unknown as DaemonClient, + serverId: host.serverId, + hostname: host.label ?? null, + }; + }, + getClientId: async () => "cid_test_removed_host", + }, + }); + await store.upsertDirectConnection({ serverId: "srv_removed", endpoint: "lan:6767" }); + await vi.waitFor(() => expect(connectCalls).toBe(1)); + + await store.removeHost("srv_removed"); + firstProbe.resolve(); + await vi.advanceTimersByTimeAsync(60_000); + + expect(connectCalls).toBe(1); + expect(store.getSnapshot("srv_removed")).toBeNull(); + expect(useSessionStore.getState().sessions["srv_removed"]).toBeUndefined(); + }); + it("saves and reconnects a rejected saved host after changing its password", async () => { const store = new HostRuntimeStore({ storage: createMemoryHostRuntimeStorage(), diff --git a/packages/app/src/runtime/host-runtime.ts b/packages/app/src/runtime/host-runtime.ts index 7328e7e5b4..14668aa0b5 100644 --- a/packages/app/src/runtime/host-runtime.ts +++ b/packages/app/src/runtime/host-runtime.ts @@ -677,8 +677,15 @@ export class HostRuntimeController { connectionId: options.initialConnection.connectionId, existingClient: options.initialConnection.existingClient, }); + if (!this.started) { + return; + } } await this.runProbeCycleNow(); + // stop() can run while startup is pending; a stopped controller never probes again. + if (!this.started) { + return; + } if (options?.autoProbe !== false) { this.probeIntervalHandle = setInterval(() => { void this.runProbeCycleNow(); From 5599f9e567128a1240b3b15afab28bceef9d36a5 Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Wed, 30 Sep 2026 02:55:44 +0200 Subject: [PATCH 012/100] fix(cli): fail agent open for an agent the daemon does not have (#5727) paseo agent open launched Desktop with whatever string it was given, printed "opened" and exited 0, so a bad ID moved the user to a broken "Agent not found" tab and the calling script saw success. Look the agent up on the daemon first, the way inspect and mode do, and open Desktop on the ID the daemon resolved. Closes #5721 Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> --- packages/cli/src/commands/agent/open.ts | 36 +++++++++++++--- packages/cli/tests/42-agent-open.test.ts | 52 ++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 6 deletions(-) create mode 100644 packages/cli/tests/42-agent-open.test.ts diff --git a/packages/cli/src/commands/agent/open.ts b/packages/cli/src/commands/agent/open.ts index 2119bd130b..2c1eab36d2 100644 --- a/packages/cli/src/commands/agent/open.ts +++ b/packages/cli/src/commands/agent/open.ts @@ -1,5 +1,6 @@ import type { Command } from "commander"; import { connectToDaemon } from "../../utils/client.js"; +import type { AgentDeepLinkTarget } from "@getpaseo/protocol/agent-deep-link"; import { openDesktopWithAgent } from "../open.js"; import type { CommandError, @@ -30,10 +31,21 @@ export function addOpenOptions(command: Command): Command { .option("--server ", "Server ID (defaults to the local daemon)"); } -async function resolveServerId(options: CommandOptions): Promise { +function toOpenError(err: unknown): CommandError { + if (err && typeof err === "object" && "code" in err) { + return err as CommandError; + } + const message = err instanceof Error ? err.message : String(err); + return { code: "OPEN_FAILED", message: `Failed to open agent: ${message}` }; +} + +async function resolveAgentTarget( + agentId: string, + options: CommandOptions, +): Promise { const explicitServerId = typeof options.server === "string" ? options.server.trim() : ""; if (explicitServerId) { - return explicitServerId; + return { serverId: explicitServerId, agentId }; } const client = await connectToDaemon({ target: options.daemonTarget }); @@ -46,7 +58,19 @@ async function resolveServerId(options: CommandOptions): Promise { }; throw error; } - return serverId; + + const fetchResult = await client.fetchAgent({ agentId }); + if (!fetchResult) { + const error: CommandError = { + code: "AGENT_NOT_FOUND", + message: `Agent not found: ${agentId}`, + details: 'Use "paseo ls" to list available agents', + }; + throw error; + } + return { serverId, agentId: fetchResult.agent.id }; + } catch (err) { + throw toOpenError(err); } finally { await client.close().catch(() => {}); } @@ -66,12 +90,12 @@ export async function runOpenCommand( throw error; } - const serverId = await resolveServerId(options); - await openDesktopWithAgent({ serverId, agentId }); + const target = await resolveAgentTarget(agentId, options); + await openDesktopWithAgent(target); return { type: "single", - data: { agentId, serverId, status: "opened" }, + data: { ...target, status: "opened" }, schema: openAgentSchema, }; } diff --git a/packages/cli/tests/42-agent-open.test.ts b/packages/cli/tests/42-agent-open.test.ts new file mode 100644 index 0000000000..d468dfe16d --- /dev/null +++ b/packages/cli/tests/42-agent-open.test.ts @@ -0,0 +1,52 @@ +#!/usr/bin/env npx tsx + +import assert from "node:assert"; +import { chmod, mkdir, readFile, writeFile } from "node:fs/promises"; +import { existsSync } from "node:fs"; +import { join } from "node:path"; +import { runPaseoCli, startTestDaemon } from "./helpers/test-daemon.ts"; + +console.log("=== Agent Open Command Tests ===\n"); + +if (process.platform !== "linux") { + console.log("Skipped: the fake Desktop app is a Linux AppImage path."); + process.exit(0); +} + +const daemon = await startTestDaemon(); + +// runPaseoCli sets HOME to the daemon's home, where the CLI looks for +// ~/Applications/Paseo.AppImage on Linux. The fake app records its launch. +const launchRecord = join(daemon.paseoHome, "desktop-launches.txt"); +const fakeDesktop = join(daemon.paseoHome, "Applications", "Paseo.AppImage"); +await mkdir(join(daemon.paseoHome, "Applications"), { recursive: true }); +await writeFile(fakeDesktop, `#!/bin/sh\necho "$@" >> "${launchRecord}"\n`); +await chmod(fakeDesktop, 0o755); + +try { + { + console.log("Test 1: an unknown agent ID fails without opening Desktop"); + const result = await runPaseoCli(daemon, [ + "agent", + "open", + "does-not-exist", + "--host", + `127.0.0.1:${daemon.port}`, + "--json", + ]); + + assert.notStrictEqual(result.exitCode, 0, "open should fail for an unknown agent"); + assert.match(JSON.parse(result.stderr).error.message, /Agent not found: does-not-exist/); + // Give a wrongly spawned detached app time to write its record. + await new Promise((resolve) => setTimeout(resolve, 500)); + assert.ok( + !existsSync(launchRecord), + `Desktop should not open, but was launched with: ${existsSync(launchRecord) ? await readFile(launchRecord, "utf8") : ""}`, + ); + console.log("✓ unknown agent ID fails without opening Desktop\n"); + } +} finally { + await daemon.stop(); +} + +console.log("=== All agent open tests passed ==="); From b1a3b0772d15813a70b7665da27ebca4cbed329a Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Wed, 30 Sep 2026 12:10:14 +0200 Subject: [PATCH 013/100] fix(claude): skip user and project hooks for internal agents (#5750) Branch-name, commit message, and pull request text generation run as internal Claude agents in the workspace, so a project Stop hook that pushes HEAD published the placeholder branch before the rename. Closes #5742 Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> --- .../agent-response-loop.real.e2e.test.ts | 37 ++++++++++++++++++- .../agent/providers/claude/agent.test.ts | 29 +++++++++++++++ .../server/agent/providers/claude/agent.ts | 6 ++- 3 files changed, 70 insertions(+), 2 deletions(-) diff --git a/packages/server/src/server/agent/agent-response-loop.real.e2e.test.ts b/packages/server/src/server/agent/agent-response-loop.real.e2e.test.ts index ec9dbfdb10..a92e547edc 100644 --- a/packages/server/src/server/agent/agent-response-loop.real.e2e.test.ts +++ b/packages/server/src/server/agent/agent-response-loop.real.e2e.test.ts @@ -1,6 +1,6 @@ import { describe, test, expect, beforeEach, afterEach, beforeAll, afterAll } from "vitest"; import { createServer } from "http"; -import { mkdtempSync, rmSync } from "fs"; +import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "fs"; import { tmpdir } from "os"; import path from "path"; import { randomUUID } from "crypto"; @@ -250,4 +250,39 @@ describe("getStructuredAgentResponse (e2e)", () => { expect(result.message.trim().toLowerCase()).toBe("hello"); }, 180000); + + test("does not run the project's Claude hooks for an internal Claude agent", async (context) => { + if (!canRunClaude) { + context.skip(); + } + const hookMarker = path.join(cwd, "stop-hook-ran"); + mkdirSync(path.join(cwd, ".claude")); + writeFileSync( + path.join(cwd, ".claude", "settings.json"), + JSON.stringify({ + hooks: { + Stop: [{ hooks: [{ type: "command", command: `touch '${hookMarker}'` }] }], + }, + }), + ); + + const result = await generateStructuredAgentResponse({ + manager, + agentConfig: { + provider: "claude", + model: CLAUDE_TEST_MODEL, + cwd, + title: "Branch name generator", + internal: true, + }, + prompt: + 'Respond with exactly this JSON (no markdown, no extra keys, no extra text): {"message":"hello"}', + schema: z.object({ message: z.string() }), + maxRetries: 2, + persistSession: false, + }); + + expect(result.message.trim().toLowerCase()).toBe("hello"); + expect(existsSync(hookMarker)).toBe(false); + }, 180000); }); diff --git a/packages/server/src/server/agent/providers/claude/agent.test.ts b/packages/server/src/server/agent/providers/claude/agent.test.ts index 16aa193cce..3e91b9d639 100644 --- a/packages/server/src/server/agent/providers/claude/agent.test.ts +++ b/packages/server/src/server/agent/providers/claude/agent.test.ts @@ -1067,6 +1067,35 @@ describe("ClaudeAgentSession features", () => { await session.close(); }); + test("disables Claude hooks for internal agents only", async () => { + const { queryFactory } = createQueryMock(); + const client = new ClaudeAgentClient({ + logger, + queryFactory, + resolveBinary: async () => "/test/claude/bin", + }); + const internalSession = await client.createSession({ + provider: "claude", + cwd: process.cwd(), + internal: true, + }); + const userSession = await client.createSession({ + provider: "claude", + cwd: process.cwd(), + }); + + await internalSession.startTurn("hello"); + await userSession.startTurn("hello"); + + expect(queryFactory.mock.calls[0]?.[0].options.settings).toMatchObject({ + disableAllHooks: true, + }); + expect(queryFactory.mock.calls[1]?.[0].options.settings).toBeUndefined(); + + await internalSession.close(); + await userSession.close(); + }); + test("turns Claude thinking off without retaining an effort level", async () => { const { queryFactory, launches } = createQueryMock(); const client = new ClaudeAgentClient({ diff --git a/packages/server/src/server/agent/providers/claude/agent.ts b/packages/server/src/server/agent/providers/claude/agent.ts index 38476490b3..7eecf56c96 100644 --- a/packages/server/src/server/agent/providers/claude/agent.ts +++ b/packages/server/src/server/agent/providers/claude/agent.ts @@ -3363,13 +3363,17 @@ class ClaudeAgentSession implements AgentSession { input: { ultracode: boolean }, ): Pick | Record { const fastMode = this.resolveFastModeSetting(); - if (fastMode === null && !input.ultracode) { + // Internal agents do daemon work such as naming a branch, so the user's and + // project's hooks must not run for them. + const disableAllHooks = this.config.internal === true; + if (fastMode === null && !input.ultracode && !disableAllHooks) { return {}; } return { settings: mergeClaudeSettings(providerOptions.settings, { ...(fastMode === null ? {} : { fastMode }), ...(input.ultracode ? { ultracode: true } : {}), + ...(disableAllHooks ? { disableAllHooks: true } : {}), }), }; } From 3a9304607cf438cfe328b90b7d6b3a5c24832685 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Wed, 30 Sep 2026 12:52:13 +0100 Subject: [PATCH 014/100] feat: sidebar header and footer plugin items, Usage in the sidebar footer (#5685) * refactor(usage): remove agent-to-usage wiring * feat(plugins): screens, sidebar header and footer items Plugins register screens with addScreen and contribute component-based sidebar items with addSidebarHeaderItem and addSidebarFooterItem. Items get currentScreen, openScreen and openPopover, and render the SidebarRow and SidebarButton kit. The footer shares the header's ordering model through the new sidebarFooterItems preference, and Settings > Sidebar gains a Footer card. addSurface, openSurface and addSidebarItem stay as aliases with their settings keys and routes. * refactor(plugins): share resolvePluginPlatform and keep header remembered-host key Consolidate the five resolvePlatform copies into plugins/platform.ts. Header sidebar items keep the /sidebar/ remembered-host key so existing users keep their remembered host; footer items use /sidebar-footer/. * feat(usage): global usage summary, pins and used/remaining display - Device-local usage preferences (displayAs, pinned windows) in app settings. - Sidebar footer usage summary: pinned windows in pin order, one per account; opens the expanded view as a popover on desktop and a sheet on compact. - Pin checkboxes on window rows and a used/remaining toggle shared by the summary, popover, Usage screen and Settings usage section. - Usage screen shows one host with a selector when several are connected. - Extract HostSwitcher and the sidebar popover host for reuse. * fix(usage): render each usage report as its own card * feat(usage): row pinning, one title row for usage controls, even sidebar footer - Window rows are the pin toggle (checkbox role, checked state); pinned rows get a surface2 highlight, unpinned rows an interactionHighlight hover. - Usage screen, Settings section, popover and sheet put the title left and the host selector, used/remaining toggle and Refresh right (UsageControls). PageLayout gains an actions slot; the menu sheet header a trailing slot. - Sidebar footer rows share 28px height, spacing[1] gaps and one leading rail. * fix(usage): keep row hover lighter than the pinned highlight Hovering an unpinned window row shows the pinned surface2 at half opacity on a separate layer, so it never reads stronger than a pinned row. Pinned rows do not react to hover. * fix(app): defer sidebar styles and preserve layout exports in timeline test * fix(usage): open the Usage screen on desktop, drop the Usage footer icon, match git panel refresh - Desktop presses on the usage summary open the Usage screen; only compact opens the sheet. - The Usage footer icon is gone; with nothing pinned the summary renders a plain "Usage" row in the footer row style. A stored `usage` footer preference is ignored. - Refresh controls use the Changes panel's icon-only ToolbarButton with RotateCw. * fix(sidebar): keep the footer icon row fixed; footer items are rows only The footer's icon row (Hosts, Import session, Help and support, Settings) is back to main's fixed implementation: not in Settings, not reorderable, and plugins can't add to it. - Footer items are full-width rows between Add project and the icon row: the usage summary and plugin `addSidebarFooterItem` rows. The Footer card in Settings lists only those rows. - Stored `sidebarFooterItems` keys for hosts, import, help and usage are ignored. - `SidebarButton` is removed from the plugin UI kit; a footer item renders `SidebarRow`. * refactor(usage): call the sidebar footer item Usage One concept, one word: the footer item is the Usage item, with key `usage`. - Settings > Sidebar > Footer lists it as "Usage"; its preference key is `usage`. - `usage/summary-item.tsx` becomes `usage/sidebar-item.tsx` (`UsageSidebarItem`); both states use testID `sidebar-usage`. - `usage/summary.ts` becomes `usage/pinned.ts` (`resolvePinnedUsage`, `PinnedUsageWindow`): it resolves the pinned windows the item shows. * feat(plugins): screen params, several rows per sidebar item, SidebarSeparator openScreen takes { screenId, params }; params live in the screen route's query so reload, history and links keep them. currentScreen carries the params. A sidebar item may render a fragment or array of rows; a row press anchors the popover to that row. SidebarSeparator joins the plugin UI kit. Co-Authored-By: Claude * fix(plugins): any screen param key; trailing presses anchor the row's popover Screen params go in the query under a param. prefix, so keys like serverId no longer collide with the route segments Expo Router merges into search params. A press anywhere inside a SidebarRow, trailing content included, makes that row the popover anchor. Co-Authored-By: Claude * feat(plugins): plugins title their screens addScreen takes { id, title, Component }. title is a string or a function of the screen's params; the screen header shows it and follows param changes. A title function that throws or returns empty falls back to the screen id. Legacy addSidebarItem screens keep their item's title and icon, and the addSurface alias titles its screen with the id. Co-Authored-By: Claude * fix(usage): require discovery and remove unreleased agent resolver * fix(plugins): restore legacy sidebar rows, share the item runtime, header hint presses its row - Legacy addSidebarItem rows are their own group kind again: main's row, registered icon in Settings, /sidebar/ route, host remembered per row. - New-API items follow the host last used on any of the plugin's screens. - /sidebar/ falls back to a same-id screen so pre-migration links resolve. - params moves to PluginScreenProps; settings screens no longer receive it. - Sidebar items, buttons and popovers share one runtime per installation, so no empty first frame. - A header row's hint slot presses the row; labels wrap as on main. - Skill and docs corrected. Co-Authored-By: Claude * fix(usage): one host resolver, sheet reuses the screen's controls, name the host - The Usage row and screen default to the same host. - The compact sheet carries the screen's controls, host selector included. - The host selector shows with one host, and unavailable messages name the host. - The row, sheet title and pinned label use the sidebar.footer.usage translation. - Window rows read their percent and reset after the pin label. - Drop the unread per-report cache writes and headline fixture fields. Co-Authored-By: Claude * fix(plugins): popovers, header buttons and composer pills own their runtime again Only sidebar item rows use the installation's shared runtime. Popover content disposes its runtime on unmount, so observations close with the popover. * fix(plugins): a sidebar item releases its subscriptions when it unmounts Each mounted sidebar item gets its own API scope over the host's client, created during render so its first frame has content. The scope closes a microtask after unmount (StrictMode's remount keeps it), when the item moves to another host, or when the installation stops. Replaces the installation-wide shared runtime. * refactor(plugins): one Paseo client per plugin; authors own their subscriptions usePaseo() in every plugin surface, and client.paseo in setup, is the installation's client, created with the installation in the registry. Authors release their subscriptions in their own cleanup; plugin teardown disposes the client and ends the rest. Nothing is created or disposed per mounted surface: the per-surface runtimes, their boundaries, and the per-run Command Center and slash command APIs are gone. * refactor(plugins): one Paseo client per plugin; authors own their subscriptions usePaseo() in every plugin surface, and client.paseo in setup, is the installation's client, created with the installation in the registry. Authors release their subscriptions in their own cleanup; plugin teardown disposes the client and ends the rest. Nothing is created or disposed per mounted surface: the per-surface runtimes, their boundaries, and the per-run Command Center and slash command APIs are gone. * fix(usage): show usage from released hosts Normalize released-host usage responses at the client boundary so existing usage and pins remain available across app and daemon versions. Include account labels in legacy responses for older apps. * fix(usage): keep the usage capability check out of the public SDK * test(server): retry transient locks during runtime test cleanup * test(usage): model hosts without either usage capability --------- Co-authored-by: Claude --- docs/glossary.md | 3 +- docs/plugins.md | 80 ++- docs/providers.md | 2 +- .../e2e/browser/plugin-sidebar-items.spec.ts | 478 ++++++++++++++++++ .../e2e/browser/sidebar-nav-settings.spec.ts | 72 ++- .../e2e/browser/usage-composer-pill.spec.ts | 219 -------- packages/app/e2e/browser/usage-screen.spec.ts | 39 +- .../app/e2e/browser/usage-settings.spec.ts | 13 +- .../e2e/browser/usage-sidebar-item.spec.ts | 192 +++++++ .../app/e2e/support/helpers/plugin-buttons.ts | 2 +- .../support/helpers/plugin-sidebar-items.ts | 268 ++++++++++ .../support/helpers/sidebar-nav-settings.ts | 109 +++- .../app/e2e/support/helpers/usage-reports.ts | 40 +- .../e2e/support/helpers/usage-sidebar-item.ts | 111 ++++ .../app/src/agent-stream/presentation.test.ts | 2 + .../src/components/hosts/host-switcher.tsx | 83 +++ packages/app/src/components/left-sidebar.tsx | 78 +-- .../app/src/components/page-layout/index.tsx | 28 +- .../components/sidebar/sidebar-header-row.tsx | 182 ++++--- .../components/sidebar/sidebar-nav-rows.tsx | 13 +- .../components/sidebar/sidebar-popover.tsx | 98 ++++ .../src/components/ui/menu/menu-surface.tsx | 15 +- .../app/src/components/ui/status-badge.tsx | 11 +- packages/app/src/composer/index.tsx | 10 +- .../src/hooks/use-settings/storage.test.ts | 69 +++ .../app/src/hooks/use-settings/storage.ts | 13 + packages/app/src/i18n/resources/ar.ts | 14 +- packages/app/src/i18n/resources/en.ts | 14 +- packages/app/src/i18n/resources/es.ts | 16 +- packages/app/src/i18n/resources/fr.ts | 15 +- packages/app/src/i18n/resources/ja.ts | 14 +- packages/app/src/i18n/resources/ko.ts | 14 +- packages/app/src/i18n/resources/pt-BR.ts | 14 +- packages/app/src/i18n/resources/ru.ts | 16 +- packages/app/src/i18n/resources/zh-CN.ts | 13 +- packages/app/src/plugins/actions.ts | 33 +- .../app/src/plugins/buttons/model.test.ts | 5 +- packages/app/src/plugins/buttons/view.tsx | 61 +-- packages/app/src/plugins/client-runtime.ts | 17 +- .../plugins/client-slash-commands/index.ts | 53 +- .../command-center/contributions.test.ts | 39 +- .../plugins/command-center/contributions.ts | 7 +- .../plugins/command-center/registration.tsx | 6 - packages/app/src/plugins/contribution-host.ts | 14 + packages/app/src/plugins/evaluate.test.ts | 132 ++++- packages/app/src/plugins/evaluate.ts | 116 ++++- .../plugins/installation-provider.test.tsx | 71 +++ .../app/src/plugins/installation-provider.tsx | 21 + packages/app/src/plugins/navigation.ts | 6 +- packages/app/src/plugins/platform.ts | 9 + packages/app/src/plugins/popover.tsx | 59 +++ packages/app/src/plugins/react-native/ui.ts | 2 + .../src/plugins/registry-lifecycle.test.ts | 97 +++- .../src/plugins/registry-requirements.test.ts | 4 +- packages/app/src/plugins/registry.ts | 25 +- packages/app/src/plugins/routes.test.ts | 77 ++- packages/app/src/plugins/routes.ts | 66 ++- packages/app/src/plugins/runtime-boundary.tsx | 26 - packages/app/src/plugins/settings/index.tsx | 7 +- .../app/src/plugins/sidebar-groups.test.ts | 97 +++- packages/app/src/plugins/sidebar-groups.ts | 91 +++- packages/app/src/plugins/sidebar-items.tsx | 59 --- .../app/src/plugins/sidebar-items/frame.ts | 23 + .../app/src/plugins/sidebar-items/index.tsx | 245 +++++++++ .../app/src/plugins/sidebar-items/kit.tsx | 72 +++ .../app/src/plugins/sidebar-items/legacy.tsx | 44 ++ .../src/plugins/surface-contribution.test.ts | 136 ++++- .../app/src/plugins/surface-contribution.ts | 83 ++- .../app/src/plugins/surface-runtime.test.tsx | 112 ---- packages/app/src/plugins/surface-runtime.ts | 44 -- packages/app/src/plugins/surface-screen.tsx | 134 +++-- packages/app/src/plugins/theme.test.ts | 5 +- .../app/src/plugins/timeline/model.test.ts | 5 +- .../app/src/plugins/timeline/view.test.tsx | 9 +- packages/app/src/plugins/timeline/view.tsx | 19 +- packages/app/src/plugins/types.ts | 20 +- .../src/plugins/workspace-panels/panel.tsx | 23 +- .../workspace-panels/resolution.test.ts | 5 +- .../settings/sidebar/sidebar-nav-section.tsx | 47 +- packages/app/src/sidebar-nav/model.test.ts | 120 ++++- packages/app/src/sidebar-nav/model.ts | 58 ++- .../src/sidebar-nav/use-sidebar-nav-items.ts | 66 +-- packages/app/src/usage/card.tsx | 60 ++- packages/app/src/usage/composer-pill.tsx | 82 --- packages/app/src/usage/controls.tsx | 111 ++++ packages/app/src/usage/copy.ts | 8 +- packages/app/src/usage/display-toggle.tsx | 22 + packages/app/src/usage/display.ts | 46 ++ packages/app/src/usage/format.ts | 6 + packages/app/src/usage/host-usage-section.tsx | 3 + packages/app/src/usage/hosts.ts | 39 ++ packages/app/src/usage/index.ts | 2 +- packages/app/src/usage/list.tsx | 21 +- packages/app/src/usage/model.test.ts | 216 ++++---- packages/app/src/usage/model.ts | 104 ++-- packages/app/src/usage/pinned.test.ts | 123 +++++ packages/app/src/usage/pinned.ts | 44 ++ packages/app/src/usage/preferences.test.ts | 39 ++ packages/app/src/usage/preferences.ts | 48 ++ packages/app/src/usage/queries.ts | 109 +--- packages/app/src/usage/sidebar-item.tsx | 242 +++++++++ packages/app/src/usage/usage-screen.tsx | 69 ++- packages/app/src/usage/usage-section.tsx | 36 +- packages/app/src/usage/window-bar.tsx | 114 ++++- .../cli/src/commands/plugin/scaffold.test.ts | 6 +- packages/cli/src/commands/plugin/scaffold.ts | 28 +- packages/client/src/daemon-client.test.ts | 165 ++++++ packages/client/src/daemon-client.ts | 51 +- packages/plugin/src/client/contracts.ts | 63 ++- packages/plugin/src/client/index.ts | 9 + packages/plugin/src/client/ui.ts | 21 + packages/plugin/src/server/provider.ts | 23 - packages/plugin/src/server/usage.test.ts | 5 +- packages/plugin/src/server/usage.ts | 5 +- packages/protocol/src/messages.ts | 16 +- .../src/server/agent/agent-sdk-types.ts | 7 - .../src/server/agent/plugin-provider.test.ts | 18 - .../src/server/agent/plugin-provider.ts | 31 +- .../src/server/agent/provider-registry.ts | 1 - .../server/agent/providers/acp-agent.test.ts | 7 - .../src/server/agent/providers/acp-agent.ts | 4 - .../agent/providers/claude/agent.test.ts | 51 -- .../server/agent/providers/claude/agent.ts | 10 - .../providers/codex-app-server-agent.test.ts | 38 -- .../agent/providers/codex-app-server-agent.ts | 14 - .../server/agent/providers/omp/agent.test.ts | 77 +-- .../src/server/agent/providers/omp/agent.ts | 11 - .../providers/omp/test-utils/omp-harness.ts | 4 - .../providers/omp/usage-reference.test.ts | 284 ----------- .../agent/providers/omp/usage-reference.ts | 169 ------- .../agent/providers/opencode-agent.test.ts | 94 ---- .../server/agent/providers/opencode-agent.ts | 74 --- .../opencode-bridge.local.e2e.test.ts | 3 +- .../server/agent/providers/pi/agent.test.ts | 67 +-- .../src/server/agent/providers/pi/agent.ts | 56 -- .../authorization/operation-permissions.ts | 2 - packages/server/src/server/plugins/index.ts | 12 +- .../server/plugins/plugin-process-protocol.ts | 2 - .../src/server/plugins/plugin-process.ts | 5 +- .../src/server/plugins/runtime.posix.test.ts | 26 + .../session-usage-reference.e2e.test.ts | 150 ------ .../session-usage-reference/index.server.ts | 101 ---- .../session-usage-reference/paseo-plugin.json | 1 - .../usage-source-directory/index.server.ts | 2 +- .../usage-source/index.server.ts | 2 +- .../plugins/usage-sources/index.test.ts | 124 ++--- .../src/server/plugins/usage-sources/index.ts | 22 +- packages/server/src/server/session.ts | 7 - .../session/usage/usage-session.test.ts | 57 +-- .../src/server/session/usage/usage-session.ts | 50 +- plugin-examples/hosts/index.client.tsx | 13 +- plugin-examples/modal-ui/index.client.tsx | 18 +- .../server/keychain.test.ts | 25 +- .../claude-usage-source/server/usage.test.ts | 73 ++- plugins/claude-usage-source/server/usage.ts | 32 +- plugins/claude-usage-source/shared/input.ts | 6 +- .../codex-usage-source/server/usage.test.ts | 75 +-- plugins/codex-usage-source/server/usage.ts | 20 +- plugins/codex-usage-source/shared/input.ts | 6 +- plugins/grok-usage-source/server/usage.ts | 1 - plugins/kimi-usage-source/server/usage.ts | 1 - plugins/minimax-usage-source/server/usage.ts | 1 - .../server/usage.test.ts | 54 +- .../opencode-go-usage-source/server/usage.ts | 9 +- .../opencode-go-usage-source/shared/input.ts | 5 +- public-docs/plugins/index.md | 32 +- public-docs/plugins/migration.md | 41 +- public-docs/plugins/reference.md | 244 +++++++-- skills/paseo-plugin/SKILL.md | 75 +-- 169 files changed, 5735 insertions(+), 3345 deletions(-) create mode 100644 packages/app/e2e/browser/plugin-sidebar-items.spec.ts delete mode 100644 packages/app/e2e/browser/usage-composer-pill.spec.ts create mode 100644 packages/app/e2e/browser/usage-sidebar-item.spec.ts create mode 100644 packages/app/e2e/support/helpers/plugin-sidebar-items.ts create mode 100644 packages/app/e2e/support/helpers/usage-sidebar-item.ts create mode 100644 packages/app/src/components/hosts/host-switcher.tsx create mode 100644 packages/app/src/components/sidebar/sidebar-popover.tsx create mode 100644 packages/app/src/plugins/installation-provider.test.tsx create mode 100644 packages/app/src/plugins/installation-provider.tsx create mode 100644 packages/app/src/plugins/platform.ts create mode 100644 packages/app/src/plugins/popover.tsx delete mode 100644 packages/app/src/plugins/runtime-boundary.tsx delete mode 100644 packages/app/src/plugins/sidebar-items.tsx create mode 100644 packages/app/src/plugins/sidebar-items/frame.ts create mode 100644 packages/app/src/plugins/sidebar-items/index.tsx create mode 100644 packages/app/src/plugins/sidebar-items/kit.tsx create mode 100644 packages/app/src/plugins/sidebar-items/legacy.tsx delete mode 100644 packages/app/src/plugins/surface-runtime.test.tsx delete mode 100644 packages/app/src/plugins/surface-runtime.ts delete mode 100644 packages/app/src/usage/composer-pill.tsx create mode 100644 packages/app/src/usage/controls.tsx create mode 100644 packages/app/src/usage/display-toggle.tsx create mode 100644 packages/app/src/usage/display.ts create mode 100644 packages/app/src/usage/hosts.ts create mode 100644 packages/app/src/usage/pinned.test.ts create mode 100644 packages/app/src/usage/pinned.ts create mode 100644 packages/app/src/usage/preferences.test.ts create mode 100644 packages/app/src/usage/preferences.ts create mode 100644 packages/app/src/usage/sidebar-item.tsx delete mode 100644 packages/server/src/server/agent/providers/omp/usage-reference.test.ts delete mode 100644 packages/server/src/server/agent/providers/omp/usage-reference.ts delete mode 100644 packages/server/src/server/plugins/session-usage-reference.e2e.test.ts delete mode 100644 packages/server/src/server/plugins/test-fixtures/session-usage-reference/index.server.ts delete mode 100644 packages/server/src/server/plugins/test-fixtures/session-usage-reference/paseo-plugin.json diff --git a/docs/glossary.md b/docs/glossary.md index 4e0111475c..03b65db022 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -19,7 +19,7 @@ Authoritative terminology. UI label wins. Don't invent synonyms; use what's here - **MR** — GitLab merge request. UI label for GitLab change requests only; do not use MR for GitHub/Gitea/Forgejo. - **Worktree** — Paseo-managed git worktree (`~/.paseo/worktrees/{name}`); also a `workspaceKind` value. User-facing creation treats it as the `worktree` workspace isolation choice. Code and `paseo.json` retain worktree terminology for git lifecycle implementation. Forbidden: "Checkout" as a product synonym. - **Repository / Remote** — Internal Git observations. They may produce mutable kind, branch, and project-grouping metadata but never the host-local project ID, root, display name, or workspace membership. No UI label. -- **Sidebar items** — The top-level rows at the head of the app sidebar: New workspace, History, Search, Schedules, and every plugin sidebar contribution. Order and visibility are one per-device preference (`AppSettings.sidebarNavItems`); all of them may be off. UI: the Sidebar settings page. Code: `packages/app/src/sidebar-nav/model.ts` resolves the list, and both the sidebar (`components/sidebar/sidebar-nav-rows.tsx`) and the settings section (`screens/settings/sidebar/sidebar-nav-section.tsx`) read it through `useSidebarNavItems`. Never hardcode one of these rows or add a second place that decides which ones show. Don't confuse with **Explorer sidebar**. +- **Sidebar items** — The configurable entries of the app sidebar, in two sections. Header: the rows at the head of the sidebar — New workspace, History, Search, Schedules, and plugin header items. Footer: the full-width rows between Add project and the footer icon row — Usage and plugin footer items. Add project and the icon row (Hosts, Import session, Help and support, Settings) are fixed and are not sidebar items. A plugin item may render several rows, such as one per bot; it is still one item that moves and hides as a block. Each section's order and visibility is one per-device preference (`AppSettings.sidebarNavItems`, `AppSettings.sidebarFooterItems`); every item may be off. UI: the Header and Footer cards on the Sidebar settings page. Code: `packages/app/src/sidebar-nav/model.ts` resolves a section, and the sidebar (`components/sidebar/sidebar-nav-rows.tsx`, the footer in `components/left-sidebar.tsx`) and the settings page (`screens/settings/sidebar/sidebar-nav-section.tsx`) read it through `useSidebarNavItems(section)`. Never hardcode one of these entries or add a second place that decides which ones show. Don't confuse with **Explorer sidebar**. - **Explorer sidebar** — The dedicated navigation dock containing the Files and Changes trees. UI: "Explorer sidebar". Code: `packages/app/src/workspace-tabs/explorer-sidebar.ts`; persisted pane identifiers retain the `explorer` spelling. See [explorer-sidebar.md](explorer-sidebar.md). - **Side pane** — An ordinary right-side workspace pane used for content opened beside the user's work. It closes and participates in workspace splits like every other main-host pane. See [explorer-sidebar.md](explorer-sidebar.md#side-pane). Forbidden: "Side panel". - **Directory-backed surface** — A right-sidebar surface whose content is determined by the workspace's `cwd`, so two workspaces on the same directory see identical content: git diff/status, forge change-request info, file preview/explorer contents. Keyed by `(serverId, cwd)`, never `workspaceId`. See [architecture.md](architecture.md#right-sidebar-boundary-directory-backed-vs-workspace-owned). @@ -34,6 +34,7 @@ Authoritative terminology. UI label wins. Don't invent synonyms; use what's here - **Usage source** — Plugin registration that discovers accounts and fetches plan usage. Code: `UsageSourceRegistration` (`packages/plugin/src/server/usage.ts`). - **Report ID** — Stable daemon-composed identity for one source account: `:`. The source owns the account key; clients treat the whole ID as opaque. Code: `UsageReportEntry.id` (`packages/protocol/src/messages.ts`). - **Usage report** — A source's normalized usage for one account, including status and optional windows, balances, and details. Its entry carries the Report ID, account label, and fetch time. Code: `UsageReport` (`packages/plugin/src/server/usage.ts`). +- **Usage (sidebar item)** — The sidebar footer item showing each pinned window's source icon and percent, or a plain "Usage" row while no pinned window has data; pressing it opens the Usage screen (a bottom sheet on phones). A pin is `{ sourceId, windowId }` and shows once per account of that source. Pins and the used/remaining choice are one per-device preference (`AppSettings.usage`) that every usage surface follows. It reads the active workspace's host, else the first connected host with usage sources. Code: `packages/app/src/usage/sidebar-item.tsx`, `usage/pinned.ts`, `usage/preferences.ts`. - **Model** — A specific LLM offered by a provider. UI: "Model" / "Select model". Code: `AgentModelDefinition` (`packages/protocol/src/messages.ts:187`). - **Tab** — UI surface representing one session inside a workspace. Not a conceptual unit; use **Agent session** when talking about the model. Code: `WorkspaceTabDescriptor` (`packages/app/src/screens/workspace/workspace-tabs-types.ts`). - **Terminal** — Workspace-scoped PTY shell streamed over the binary mux channel. UI: "Terminal". Code: `TerminalStreamFrame` (`packages/protocol/src/terminal-stream-protocol.ts`). diff --git a/docs/plugins.md b/docs/plugins.md index 370d3a2bdc..cf08b2a62a 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -259,12 +259,23 @@ export default function contribute(server: PluginServerContext) { ```tsx // index.client.tsx -import type { PluginClientContext } from "@getpaseo/plugin/client"; +import type { PluginClientContext, PluginSidebarItemProps } from "@getpaseo/plugin/client"; +import { SidebarRow } from "@getpaseo/plugin/client/ui"; import { Greeting } from "./client/greeting"; +function GreetingItem({ currentScreen, openScreen }: PluginSidebarItemProps) { + return ( + openScreen({ screenId: "main" })} + /> + ); +} + export default function contribute(client: PluginClientContext) { - client.addSurface("main", Greeting); - client.addSidebarItem({ id: "main", title: "Greeting", icon: "MessageCircle", surface: "main" }); + client.addScreen({ id: "main", title: "Greeting", Component: Greeting }); + client.addSidebarHeaderItem({ id: "main", title: "Greeting", Component: GreetingItem }); return () => {}; } ``` @@ -277,15 +288,33 @@ errors are logged and do not interrupt host teardown. Paseo owns the route, screen header, Lucide icon validation, close action, theme DTO, layout facts, and render error boundary. The contributed component owns the complete body below the header. +The plugin owns the header's text through the screen's `title` (`resolvePluginScreenTitle` in +`plugins/surface-contribution.ts`). A legacy `addSidebarItem` route (`/sidebar/`) shows the +item's title and icon instead, as before screens had titles; the `addSurface` alias titles its +screen with the id. A `/sidebar/` route with no legacy item of that id opens the screen with +that id, so links saved before a plugin migrated keep working. + +Screen params ride in the screen route's query string (`plugins/routes.ts`), not in app state, so +reload, history, links, and the host switcher keep them without a store. Expo Router merges the +route's segments (`serverId`, `pluginId`, ...) into the same search params as the query, so param +keys are stored under a `param.` prefix; plugins never see it and may use any key. +`openScreen` validates its input once (`parsePluginOpenScreenInput`); code past it trusts the +params. RPC contracts validate inputs and outputs in both the app and plugin subprocess. `useRpc` returns a typed async function. Use the host-provided `@tanstack/react-query` for request state and caching; Paseo gives each plugin installation its own query client. `usePaseo()` and the handler's `{ paseo }` context expose the same `PaseoApi`: projects, -workspaces, agents, terminals, providers, and daemon config. They do not expose connection lifecycle. A surface borrows the -selected host's existing connection; switching the screen's host changes both `usePaseo()` and -`useRpc()` to that host. An offline selected host fails there and never falls through to another +workspaces, agents, terminals, providers, and daemon config. They do not expose connection lifecycle. + +`usePaseo()` is the plugin's one client: `InstalledPlugin.paseo`, created with the installation in +`packages/app/src/plugins/registry.ts` over the host's existing connection and passed to setup as +`client.paseo`. Authors release their subscriptions in their own cleanup, and teardown (disable, +reload, removal, host removal) disposes the client, which ends the rest; nothing is created or +disposed per mounted surface. See the +[public example](../public-docs/plugins/reference.md#use-the-paseo-sdk). Switching the screen's host +changes both `usePaseo()` and `useRpc()` to that host's installation. An offline selected host fails there and never falls through to another installation. A server handler owns an IPC-backed daemon session for the life of its subprocess. Use plugin RPC for plugin-specific backend behavior that is not a normal Paseo operation. @@ -321,7 +350,7 @@ existing agent-context instances, but it cannot create an agent panel without an Command Center callbacks use the selected host's existing `PaseoApi` for normal Paseo operations. They use typed plugin RPC only for plugin-specific backend work. Surface and panel navigation belongs to the app; plugins do not receive Expo Router or workspace-layout store access. -See the public [navigation fields](../public-docs/plugins/reference.md#surfaces-and-sidebar-items) +See the public [navigation fields](../public-docs/plugins/reference.md#screens-and-sidebar-items) and [external links and workspace browsers](../public-docs/plugins/reference.md#external-links-and-workspace-browsers) for the author-facing contract. @@ -392,6 +421,41 @@ Register a usage source from `index.server.ts` with `server.registerUsageSource( The daemon calls discovery for `usage.list_reports`; the client gates this RPC on `server_info.features.usageSources`. The old `provider.usage.list` RPC maps discovered reports for older clients. See the [public usage source reference](../public-docs/plugins/reference.md#usage-sources) for the author contract and minimum version. +## Contribute sidebar items + +Sidebar header and footer items are plugin components, not descriptors. The +[public reference](../public-docs/plugins/reference.md#sidebar-items) owns the author API. The host +lives in `packages/app/src/plugins/sidebar-items/`; `packages/app/src/sidebar-nav/model.ts` resolves +one section's order from built-ins, plugin groups, and the section's preference +(`sidebarNavItems` or `sidebarFooterItems`). + +- The item's `Component` renders directly in the section, with no wrapper, so a fragment or array + of rows lays out like separate items while Settings keeps one entry for the block. Footer items + are rows between Add project and the footer's icon row. The icon row (Hosts, Import session, + Help and support, Settings) is fixed app code, not a contribution slot, so the kit has no icon + button. +- `openPopover` opens through the same `PluginPopoverSurface` as header buttons + (`plugins/popover.tsx`). Any press inside a `SidebarRow`, including its `trailing` content, moves + the menu anchor to that row: a capture-phase `onStartShouldSetResponderCapture` runs before the + pressed child claims the touch on native and web, and returns false. Keyboard activation skips + the responder system, so the row's own press anchors too. Until a press, the first mounted row + holds the anchor. An item that renders no kit component + anchors to its section container. +- `SidebarSeparator` reuses the app's separator and cancels the footer's horizontal padding, so + the line runs edge to edge in both sections. +- `SidebarRow.trailing` sits in a second `Pressable` beside the row's button, since web cannot nest + buttons. Its `onPress` is the row's, so a press on non-interactive trailing content presses the + row; a button inside it wins its own press because only the innermost pressable responds. +- A new-API item renders from the current route's host, else the host remembered under + `pluginScreensHostKey(pluginId)`, which the screen's host switcher and the item's `openScreen` + set. One key per plugin: switching host on any of its screens moves all its items. +- `addSurface`, `openSurface`, and `addSidebarItem` are undocumented aliases, tagged + `COMPAT(pluginSidebarAliases)`. `addSidebarItem` is recorded only in `legacySidebarItems` and + groups as a `legacy` sidebar group, which the app draws as its own row: its registered icon, in + the sidebar and in Settings > Sidebar, opening `/plugin//sidebar/` on the host it + remembers under its own key. It shares header ids and the `plugin::` settings key + with new-API items. + ## Contribute buttons Header buttons and composer pills share the client-only descriptor and registration lifecycle in @@ -461,7 +525,7 @@ be rendered intact. The daemon advertises this RPC through `addSlashCommand` registers an agent- or workspace-context command in the composer. The callback runs in the app, receives the trimmed text after the command name as `args`, and receives -the same `paseo`, `rpc`, `openSurface`, workspace, agent, and `openPanel` capabilities as the matching +the same `paseo`, `rpc`, `openScreen`, workspace, agent, and `openPanel` capabilities as the matching Command Center callback. ```ts diff --git a/docs/providers.md b/docs/providers.md index 6ea928f3c9..fa608ba7f0 100644 --- a/docs/providers.md +++ b/docs/providers.md @@ -199,7 +199,7 @@ promise for completion: equal results, including equal discovery timestamps, emi ## Usage sources -Usage is fetched on demand from plugin usage sources. Each source registers through `server.registerUsageSource()` with an input schema, `fetch(input)`, and optional `discover()`. The daemon discovers configured accounts, validates inputs in the plugin runtime, caches each source/input result for five minutes, and returns `usage.list_reports.response`. A source report has an account key, availability status, plan label, windows, balances, and details. Mark the window the app should show first with `headline: true`. +Usage is fetched on demand from plugin usage sources. Each source registers through `server.registerUsageSource()` with an input schema, `fetch(input)`, and required `discover()`. The daemon discovers configured accounts, validates inputs in the plugin runtime, caches each source/input result for five minutes, and returns `usage.list_reports.response`. A source report has an account key, availability status, plan label, windows, balances, and details. Create a built-in source under `plugins/-usage-source/` with the same manifest, entry, `server/`, `shared/`, and `icon.svg` layout as an external plugin. Add its ID to `builtinPlugins` in `packages/server/src/server/plugins/builtin/index.ts`. Keep credential discovery, API parsing, and normalization inside the source; use helpers from `@getpaseo/plugin/server/usage`. The wire shape remains source agnostic. See [plugin usage sources](plugins.md#usage-sources). diff --git a/packages/app/e2e/browser/plugin-sidebar-items.spec.ts b/packages/app/e2e/browser/plugin-sidebar-items.spec.ts new file mode 100644 index 0000000000..24b90bfe4e --- /dev/null +++ b/packages/app/e2e/browser/plugin-sidebar-items.spec.ts @@ -0,0 +1,478 @@ +import path from "node:path"; +import type { Locator, Page } from "@playwright/test"; +import { expect, test } from "../support/fixtures"; +import { gotoWorkspace } from "../support/helpers/launcher"; +import { + BOTS_PLUGIN_ID, + COMPACT, + LEGACY_PLUGIN_ID, + SHOWCASE_PLUGIN_ID, + WIDE, + closeScreen, + expectRowActive, + footerItem, + headerRow, + installSidebarPlugins, + popoverBox, + runCommand, + visibleTestId, +} from "../support/helpers/plugin-sidebar-items"; +import { + leaveSettings, + openSidebarNavSettings, + setFooterItemVisible, +} from "../support/helpers/sidebar-nav-settings"; +import { seedWorkspace } from "../support/helpers/seed-client"; +import { getServerId } from "../support/helpers/server-id"; + +const APP_SETTINGS_KEY = "@paseo:app-settings"; + +/** Set PASEO_QA_SCREENSHOT_DIR to keep QA screenshots of each state. */ +async function qaScreenshot(page: Page, name: string, area?: Locator) { + const directory = process.env.PASEO_QA_SCREENSHOT_DIR; + if (!directory) return; + // Let popover, sheet and drawer animations settle so the image shows the final state. + await page.waitForTimeout(600); + // Expo's fast-refresh indicator sits over the footer's Hosts icon. + await page.addStyleTag({ content: ".__expo_fast_refresh { display: none !important; }" }); + const file = path.join(directory, `${name}.png`); + if (area) await area.screenshot({ path: file }); + else await page.screenshot({ path: file }); +} + +/** Footer rows sit between the fixed Add project row and the fixed icon row, full width. */ +async function expectFooterRow(page: Page, row: Locator) { + // Polled: on compact the drawer is still sliding in when the row first shows. + await expect + .poll(async () => { + const addProject = (await visibleTestId(page, "sidebar-add-project").boundingBox())!; + const hosts = (await visibleTestId(page, "sidebar-hosts-trigger").boundingBox())!; + const box = (await row.boundingBox())!; + return { + belowAddProject: box.y >= addProject.y + addProject.height, + aboveIconRow: box.y + box.height <= hosts.y, + alignedLeft: Math.abs(box.x - addProject.x) < 1, + fullWidth: Math.abs(box.width - addProject.width) < 1, + }; + }) + .toEqual({ belowAddProject: true, aboveIconRow: true, alignedLeft: true, fullWidth: true }); +} + +function botRow(page: Page, rowId: string): Locator { + return headerRow(page, BOTS_PLUGIN_ID, `bots-${rowId}`); +} + +async function boxOf(locator: Locator) { + const result = await locator.boundingBox(); + expect(result).not.toBeNull(); + return result!; +} + +/** The open screen's header title; a pushed screen keeps the one below it mounted. */ +function screenTitle(page: Page): Locator { + return page.locator('[data-testid="plugin-surface-title"]:visible').last(); +} + +async function expectBotScreen(page: Page, botId: string) { + await expect(page).toHaveURL( + new RegExp(`/plugin/${BOTS_PLUGIN_ID}/surface/bot\\?param\\.botId=${botId}$`), + ); + await expect(page.getByText(`Bot screen: ${botId}`, { exact: true })).toBeVisible(); + // The plugin titles the screen from its params: the bot's name. + await expect(screenTitle(page)).toHaveText(botId === "bot-1" ? "Bot 1" : "Bot 2"); + await expectRowActive(botRow(page, "bot-1"), botId === "bot-1"); + await expectRowActive(botRow(page, "bot-2"), botId === "bot-2"); + await expectRowActive(botRow(page, "status"), false); +} + +/** The popover opens to the right of `row`, level with it, and away from `elsewhere`. */ +async function expectPopoverBeside(page: Page, text: string, row: Locator, elsewhere: Locator) { + const popover = await popoverBox(page, text); + const rowBox = await boxOf(row); + const otherBox = await boxOf(elsewhere); + expect(popover.x).toBeGreaterThanOrEqual(rowBox.x + rowBox.width); + expect(Math.abs(popover.y - rowBox.y)).toBeLessThan(40); + expect(Math.abs(popover.y - otherBox.y)).toBeGreaterThan(20); +} + +/** Subscription ids the daemon confirms released, collected from the page's websocket. */ +function trackReleasedSubscriptions(page: Page): Set { + const released = new Set(); + page.on("websocket", (socket) => + socket.on("framereceived", ({ payload }) => collectRelease(released, payload)), + ); + return released; +} + +function collectRelease(released: Set, payload: string | Buffer) { + if (typeof payload !== "string") return; + const frame = JSON.parse(payload); + if (frame.message?.type === "subscription.release.response") + released.add(frame.message.payload.subscriptionId); +} + +async function expectReleased(released: Set, subscriptionId: string) { + await expect.poll(() => released.has(subscriptionId)).toBe(true); +} + +function readAlertsObservation(page: Page): Promise { + return page.evaluate( + () => (globalThis as { __botAlertsObservation?: string }).__botAlertsObservation ?? null, + ); +} + +/** The id of the observation the Bot alerts item opened after `previous`. */ +async function nextAlertsObservation(page: Page, previous: string | null): Promise { + await expect.poll(() => readAlertsObservation(page)).not.toBe(previous); + return (await readAlertsObservation(page))!; +} + +function sidebarFooter(page: Page): Locator { + return visibleTestId(page, "sidebar-add-project").locator("xpath=.."); +} + +test.describe("Plugin sidebar items", () => { + let workspaceId: string; + let cleanup: () => Promise; + + test.beforeEach(async () => { + const workspace = await seedWorkspace({ repoPrefix: "plugin-sidebar-items-" }); + const plugins = await installSidebarPlugins(); + workspaceId = workspace.workspaceId; + cleanup = async () => { + await plugins.cleanup(); + await workspace.cleanup(); + }; + }); + + test.afterEach(async () => { + await cleanup(); + }); + + test("a plugin's screen, header row, footer popover and command work together", async ({ + page, + }) => { + test.setTimeout(180_000); + await page.setViewportSize(WIDE); + await gotoWorkspace(page, workspaceId); + const row = headerRow(page, SHOWCASE_PLUGIN_ID, "deploys"); + const sync = footerItem(page, SHOWCASE_PLUGIN_ID, "sync"); + await expect(row).toBeVisible({ timeout: 30_000 }); + await expect(sync).toBeVisible(); + + await test.step("a throwing item renders nothing and leaves the sidebar working", async () => { + await expect(visibleTestId(page, `plugin-sidebar-${SHOWCASE_PLUGIN_ID}-broken`)).toHaveCount( + 0, + ); + await expect(footerItem(page, SHOWCASE_PLUGIN_ID, "broken")).toHaveCount(0); + await expect(page.getByText("Plugin failed", { exact: false })).toHaveCount(0); + await expect(visibleTestId(page, "sidebar-settings")).toBeVisible(); + }); + + await test.step("the header row highlights while its screen is open", async () => { + await expectRowActive(row, false); + await row.click(); + await expect(page).toHaveURL(new RegExp(`/plugin/${SHOWCASE_PLUGIN_ID}/surface/deploys`)); + await expect(page.getByText("Deploys screen body", { exact: true })).toBeVisible(); + await expect(screenTitle(page)).toHaveText("Deploys"); + await expectRowActive(row, true); + await qaScreenshot(page, "phase2-desktop-sidebar-screen"); + await closeScreen(page); + await expectRowActive(row, false); + }); + + await test.step("the trailing button presses on its own", async () => { + await page.getByRole("button", { name: "Refresh deploys", exact: true }).click(); + await expect(row).toHaveAccessibleName("Deploys (refreshed 1)"); + await expect(page).not.toHaveURL(/\/plugin\//); + await expect(page.getByText("Deploys screen body", { exact: true })).toHaveCount(0); + }); + + await test.step("the footer row renders as a row between Add project and the icon row", async () => { + await expect(sync).toHaveAccessibleName("Sync"); + await expectFooterRow(page, sync); + await expect(visibleTestId(page, "sidebar-usage")).toBeVisible(); + await qaScreenshot(page, "phase5-desktop-footer", sidebarFooter(page)); + }); + + await test.step("the footer row opens a popover anchored to it", async () => { + await sync.click(); + const popover = await popoverBox(page); + const button = (await sync.boundingBox())!; + await expect(page.getByText("Presentation: wide", { exact: true })).toBeVisible(); + expect(popover.y + popover.height).toBeLessThanOrEqual(button.y); + expect(button.y - popover.y).toBeLessThan(300); + expect(Math.abs(popover.x - button.x)).toBeLessThan(80); + await qaScreenshot(page, "phase2-desktop-popover"); + await page.getByRole("button", { name: "Open deploys from popover", exact: true }).click(); + await expect(page.getByText("Deploys screen body", { exact: true })).toBeVisible(); + await expect(page.getByText("Sync details", { exact: true })).toHaveCount(0); + await closeScreen(page); + }); + + await test.step("the command center item opens the screen", async () => { + await runCommand(page, "Open deploys"); + await expect(page.getByText("Deploys screen body", { exact: true })).toBeVisible(); + await expectRowActive(row, true); + await closeScreen(page); + }); + + await test.step("on a compact layout the popover is a bottom sheet", async () => { + await page.setViewportSize(COMPACT); + await page.getByRole("button", { name: "Open menu", exact: true }).first().click(); + const compactSync = footerItem(page, SHOWCASE_PLUGIN_ID, "sync"); + await expect(compactSync).toBeInViewport(); + await expectFooterRow(page, compactSync); + await expect(headerRow(page, SHOWCASE_PLUGIN_ID, "deploys")).toBeInViewport(); + await qaScreenshot(page, "phase2-compact-sidebar"); + await qaScreenshot(page, "phase5-compact-footer", sidebarFooter(page)); + await compactSync.click(); + await expect(page.getByText("Presentation: compact", { exact: true })).toBeVisible(); + const sheet = await popoverBox(page); + expect(sheet.y).toBeGreaterThan(COMPACT.height / 2); + await qaScreenshot(page, "phase2-compact-sheet"); + await page.getByRole("button", { name: "Close sync details", exact: true }).click(); + await expect(page.getByText("Sync details", { exact: true })).toHaveCount(0); + }); + }); + + test("a header row's shortcut hint presses the row", async ({ page }) => { + await page.setViewportSize(WIDE); + await gotoWorkspace(page, workspaceId); + const newWorkspace = visibleTestId(page, "sidebar-global-new-workspace"); + await expect(newWorkspace).toBeVisible({ timeout: 30_000 }); + await newWorkspace.hover(); + const hint = page.getByText("Ctrl+N", { exact: true }).locator("visible=true"); + await expect(hint).toBeVisible(); + await qaScreenshot(page, "phase7-header-row-hint", newWorkspace.locator("xpath=../..")); + await hint.click(); + await expect(page).toHaveURL(/\/new(\?|$)/); + }); + + test("a legacy plugin keeps its row, icon, order and visibility", async ({ page }) => { + test.setTimeout(180_000); + await page.addInitScript( + ({ key, sidebarNavItems }) => localStorage.setItem(key, JSON.stringify({ sidebarNavItems })), + { + key: APP_SETTINGS_KEY, + sidebarNavItems: [ + { key: `plugin:${LEGACY_PLUGIN_ID}:entry`, visible: true }, + { key: "new-workspace", visible: true }, + { key: `plugin:${LEGACY_PLUGIN_ID}:hidden`, visible: false }, + ], + }, + ); + await page.setViewportSize(WIDE); + await gotoWorkspace(page, workspaceId); + const entry = headerRow(page, LEGACY_PLUGIN_ID, "entry"); + await expect(entry).toBeVisible({ timeout: 30_000 }); + let entryIcon = ""; + + await test.step("the saved order and visibility still apply", async () => { + await expect(entry).toHaveAccessibleName("Legacy entry"); + await expect(entry.locator("svg")).toHaveCount(1); + entryIcon = await entry.locator("svg").innerHTML(); + await expect(headerRow(page, LEGACY_PLUGIN_ID, "hidden")).toHaveCount(0); + const entryBox = (await entry.boundingBox())!; + const newWorkspaceBox = (await visibleTestId( + page, + "sidebar-global-new-workspace", + ).boundingBox())!; + expect(entryBox.y).toBeLessThan(newWorkspaceBox.y); + }); + + await test.step("the row opens its surface on its own sidebar route and highlights", async () => { + await entry.click(); + await expect(page).toHaveURL(new RegExp(`/plugin/${LEGACY_PLUGIN_ID}/sidebar/entry$`)); + await expect(page.getByText("Legacy screen body", { exact: true })).toBeVisible(); + await expect(page.getByTestId("plugin-surface-close")).toBeVisible(); + await expect(screenTitle(page)).toHaveText("Legacy entry"); + await expectRowActive(entry, true); + await closeScreen(page); + }); + + await test.step("remembered sidebar routes keep resolving", async () => { + await page.goto( + `/h/${encodeURIComponent(getServerId())}/plugin/${LEGACY_PLUGIN_ID}/sidebar/entry`, + ); + await expect(page.getByText("Legacy screen body", { exact: true })).toBeVisible(); + await expectRowActive(headerRow(page, LEGACY_PLUGIN_ID, "entry"), true); + }); + + await test.step("links saved before a plugin moved to addScreen open the same-id screen", async () => { + const host = encodeURIComponent(getServerId()); + await page.goto(`/h/${host}/plugin/${BOTS_PLUGIN_ID}/sidebar/bot?param.botId=bot-2`); + await expect(page.getByText("Bot screen: bot-2", { exact: true })).toBeVisible({ + timeout: 30_000, + }); + await expect(screenTitle(page)).toHaveText("Bot 2"); + await page.goto(`/h/${host}/plugin/${BOTS_PLUGIN_ID}/bot`); + await expect(page).toHaveURL(new RegExp(`/plugin/${BOTS_PLUGIN_ID}/sidebar/bot$`)); + await expect(screenTitle(page)).toHaveText("Bot"); + }); + + await test.step("settings shows a legacy row's own icon and one generic icon for the rest", async () => { + await openSidebarNavSettings(page); + const legacyIcon = page + .getByTestId(`sidebar-nav-item-plugin:${LEGACY_PLUGIN_ID}:entry`) + .locator("svg") + .first(); + const showcaseIcon = page + .getByTestId(`sidebar-nav-item-plugin:${SHOWCASE_PLUGIN_ID}:deploys`) + .locator("svg") + .first(); + const footerIcon = page + .getByTestId("sidebar-nav-section-footer") + .getByTestId(`sidebar-nav-item-plugin:${SHOWCASE_PLUGIN_ID}:sync`) + .locator("svg") + .first(); + const markup = await showcaseIcon.innerHTML(); + expect(await footerIcon.innerHTML()).toBe(markup); + expect(await legacyIcon.innerHTML()).not.toBe(markup); + // The same registered icon the sidebar row draws. + expect(await legacyIcon.innerHTML()).toBe(entryIcon); + await qaScreenshot(page, "phase2-desktop-settings-sidebar"); + await qaScreenshot( + page, + "phase5-settings-sidebar-footer", + page.getByTestId("sidebar-nav-section-footer"), + ); + await page.setViewportSize(COMPACT); + await expect(page.getByTestId("sidebar-nav-section-footer").first()).toBeAttached(); + await qaScreenshot(page, "phase2-compact-settings-sidebar"); + }); + }); + test("one item renders a group of rows, and each row opens its own screen params", async ({ + page, + }) => { + test.setTimeout(180_000); + await page.setViewportSize(WIDE); + await gotoWorkspace(page, workspaceId); + const bot1 = botRow(page, "bot-1"); + const bot2 = botRow(page, "bot-2"); + const status = botRow(page, "status"); + await expect(bot1).toBeVisible({ timeout: 30_000 }); + + await test.step("the rows and the separator render in order in the header", async () => { + await expect(bot1).toHaveAccessibleName("Bot 1"); + await expect(bot2).toHaveAccessibleName("Bot 2"); + await expect(status).toHaveAccessibleName("Bot status"); + const [first, second, third] = [await boxOf(bot1), await boxOf(bot2), await boxOf(status)]; + expect(second.y).toBeGreaterThan(first.y); + // The separator adds its line and margins between Bot 2 and the status row. + expect(third.y - (second.y + second.height)).toBeGreaterThan(8); + expect(second.y - (first.y + first.height)).toBeLessThan(4); + }); + + await test.step("pressing Bot 2 opens the bot screen and highlights only its row", async () => { + await bot2.click(); + await expectBotScreen(page, "bot-2"); + await qaScreenshot(page, "phase6-desktop-sidebar-bots"); + }); + + await test.step("a reload keeps the screen and its params", async () => { + await page.reload(); + await expectBotScreen(page, "bot-2"); + await qaScreenshot(page, "phase6-bot-screen"); + }); + + await test.step("Bot 1 replaces Bot 2 on the same screen, and back returns to Bot 2", async () => { + await bot1.click(); + await expectBotScreen(page, "bot-1"); + await page.goBack(); + await expectBotScreen(page, "bot-2"); + }); + + await test.step("a popover opened from a later row anchors to that row", async () => { + await status.click(); + await expectPopoverBeside(page, "Bot status details", status, bot1); + await page.getByRole("button", { name: "Close bot status", exact: true }).click(); + await expect(page.getByText("Bot status details", { exact: true })).toHaveCount(0); + }); + + await test.step("a trailing button's popover anchors to its own row, not the last pressed", async () => { + await page.getByRole("button", { name: "More for Bot 2", exact: true }).click(); + await expectPopoverBeside(page, "Bot 2 options", bot2, status); + await qaScreenshot(page, "phase6-desktop-popover-row"); + await page.getByRole("button", { name: "Close Bot 2 options", exact: true }).click(); + await expect(page.getByText("Bot 2 options", { exact: true })).toHaveCount(0); + }); + + await test.step("a param named like a route segment reaches the screen", async () => { + await runCommand(page, "Open bot 3 on server x"); + await expect(page.getByText("Bot screen: bot-3", { exact: true })).toBeVisible(); + await expect(page.getByText("serverId param: x", { exact: true })).toBeVisible(); + // No bot named bot-3, so the title function returns its own fallback. + await expect(screenTitle(page)).toHaveText("Bot"); + await page.reload(); + await expect(page.getByText("serverId param: x", { exact: true })).toBeVisible(); + }); + + await test.step("Settings lists the group as one item", async () => { + await openSidebarNavSettings(page); + const header = page.getByTestId("sidebar-nav-section-header"); + await expect( + header.getByTestId(`sidebar-nav-item-plugin:${BOTS_PLUGIN_ID}:bots`), + ).toHaveCount(1); + await expect(header.getByText("Bots", { exact: true })).toHaveCount(1); + await expect(header.getByText("Bot 1", { exact: true })).toHaveCount(0); + await leaveSettings(page); + }); + + await test.step("the compact sidebar shows the same group", async () => { + await page.setViewportSize(COMPACT); + await page.getByRole("button", { name: "Open menu", exact: true }).first().click(); + await expect(botRow(page, "bot-1")).toBeInViewport(); + await expect(botRow(page, "status")).toBeInViewport(); + await qaScreenshot(page, "phase6-compact-sidebar-bots"); + }); + }); + + test("a plugin adds and removes a footer item after setup", async ({ page }) => { + test.setTimeout(120_000); + await page.setViewportSize(WIDE); + await gotoWorkspace(page, workspaceId); + await expect(botRow(page, "bot-1")).toBeVisible({ timeout: 30_000 }); + const alerts = footerItem(page, BOTS_PLUGIN_ID, "alerts"); + await expect(alerts).toHaveCount(0); + + await runCommand(page, "Add bot alerts"); + await expect(alerts).toBeVisible(); + await expect(alerts).toHaveAccessibleName("Bot alerts"); + await expectFooterRow(page, alerts); + + await runCommand(page, "Remove bot alerts"); + await expect(alerts).toHaveCount(0); + }); + + test("a sidebar item's observations close when it is hidden or removed", async ({ page }) => { + test.setTimeout(120_000); + const released = trackReleasedSubscriptions(page); + const alerts = footerItem(page, BOTS_PLUGIN_ID, "alerts"); + await page.setViewportSize(WIDE); + await gotoWorkspace(page, workspaceId); + await expect(botRow(page, "bot-1")).toBeVisible({ timeout: 30_000 }); + + await runCommand(page, "Add bot alerts"); + await expect(alerts).toBeVisible(); + const first = await nextAlertsObservation(page, null); + expect(released.has(first)).toBe(false); + + await test.step("hiding the item in Settings closes its observation", async () => { + await openSidebarNavSettings(page); + await setFooterItemVisible(page, `plugin:${BOTS_PLUGIN_ID}:alerts`, false); + await expectReleased(released, first); + await setFooterItemVisible(page, `plugin:${BOTS_PLUGIN_ID}:alerts`, true); + await leaveSettings(page); + }); + + await test.step("removing the item through its remover closes its observation", async () => { + await expect(alerts).toBeVisible(); + const second = await nextAlertsObservation(page, first); + expect(released.has(second)).toBe(false); + await runCommand(page, "Remove bot alerts"); + await expect(alerts).toHaveCount(0); + await expectReleased(released, second); + }); + }); +}); diff --git a/packages/app/e2e/browser/sidebar-nav-settings.spec.ts b/packages/app/e2e/browser/sidebar-nav-settings.spec.ts index e327bbd0ca..29a6807c99 100644 --- a/packages/app/e2e/browser/sidebar-nav-settings.spec.ts +++ b/packages/app/e2e/browser/sidebar-nav-settings.spec.ts @@ -1,15 +1,23 @@ import { expect, test } from "../support/fixtures"; import { gotoAppShell } from "../support/helpers/app"; +import { SHOWCASE_PLUGIN_ID, installSidebarPlugins } from "../support/helpers/plugin-sidebar-items"; import { + expectFooterIconRow, + expectFooterItemHidden, + expectFooterOrder, + expectFooterSettingsKeys, expectSidebarItemHidden, expectSidebarNavSettingsOrder, expectSidebarNavSettingsRow, expectSidebarOrder, expectStoredSidebarNav, leaveSettings, + moveFooterItemUp, moveSidebarNavItemUp, openSidebarNavSettings, + seedSidebarFooterPreferences, seedSidebarNavPreferences, + setFooterItemVisible, setSidebarNavItemVisible, } from "../support/helpers/sidebar-nav-settings"; @@ -24,8 +32,8 @@ test.describe("Sidebar items in Appearance settings", () => { await test.step("the Sidebar section lists every item in the same order", async () => { await openSidebarNavSettings(page); // The section explains itself through the header's info tooltip, not a paragraph. - await expect(page.getByTestId("sidebar-nav-section-info")).toHaveAccessibleName( - "About Sidebar", + await expect(page.getByTestId("sidebar-nav-section-header-info")).toHaveAccessibleName( + "About Header", ); await expectSidebarNavSettingsOrder(page, [ "new-workspace", @@ -106,3 +114,63 @@ test.describe("Sidebar items in Appearance settings", () => { await expectSidebarItemHidden(page, "schedules"); }); }); + +test.describe("Sidebar footer rows in Appearance settings", () => { + const syncKey = `plugin:${SHOWCASE_PLUGIN_ID}:sync`; + const brokenKey = `plugin:${SHOWCASE_PLUGIN_ID}:broken`; + let cleanup: () => Promise; + + test.beforeEach(async () => { + ({ cleanup } = await installSidebarPlugins()); + }); + + test.afterEach(async () => { + await cleanup(); + }); + + test("owner reorders and hides footer rows; the icon row stays fixed", async ({ page }) => { + test.setTimeout(120_000); + // Keys for the fixed footer icon buttons are ignored. + await seedSidebarFooterPreferences(page, [ + { key: "help", visible: false }, + { key: "hosts", visible: false }, + { key: "import", visible: false }, + ]); + await gotoAppShell(page); + + await test.step("the Footer card lists only the footer rows", async () => { + await openSidebarNavSettings(page); + await expect(page.getByTestId("sidebar-nav-section-footer-info")).toHaveAccessibleName( + "About Footer", + ); + await expectFooterSettingsKeys(page, ["usage", syncKey, brokenKey]); + await leaveSettings(page); + await expectFooterOrder(page, ["usage", syncKey]); + await expectFooterIconRow(page); + }); + + await test.step("moving Sync up and hiding the Usage item changes the rows", async () => { + await openSidebarNavSettings(page); + await moveFooterItemUp(page, syncKey); + await setFooterItemVisible(page, "usage", false); + await expectFooterSettingsKeys(page, [syncKey, "usage", brokenKey]); + await leaveSettings(page); + await expectFooterItemHidden(page, "usage"); + await expect( + page.locator(`[data-testid="plugin-sidebar-footer-${SHOWCASE_PLUGIN_ID}-sync"]:visible`), + ).toBeVisible(); + await expectFooterIconRow(page); + }); + + await test.step("the rows keep that shape across a reload", async () => { + await page.reload(); + await expect( + page.locator(`[data-testid="plugin-sidebar-footer-${SHOWCASE_PLUGIN_ID}-sync"]:visible`), + ).toBeVisible({ timeout: 30_000 }); + await expectFooterItemHidden(page, "usage"); + await expectFooterIconRow(page); + await openSidebarNavSettings(page); + await expectFooterSettingsKeys(page, [syncKey, "usage", brokenKey]); + }); + }); +}); diff --git a/packages/app/e2e/browser/usage-composer-pill.spec.ts b/packages/app/e2e/browser/usage-composer-pill.spec.ts deleted file mode 100644 index 42df720db7..0000000000 --- a/packages/app/e2e/browser/usage-composer-pill.spec.ts +++ /dev/null @@ -1,219 +0,0 @@ -import type { UsageReportEntry } from "@getpaseo/protocol/messages"; -import { expect, test, type Page } from "../support/fixtures"; -import { expectComposerVisible, submitMessage } from "../support/helpers/composer"; -import { openAgentRoute, seedMockAgentWorkspace } from "../support/helpers/mock-agent"; -import { installUsageReportsFixture } from "../support/helpers/usage-reports"; - -const REPORT_ID = "fixture:fixture-account"; - -// Two hours reads "2h ago" for an hour, so the assertion cannot race the clock. -function twoHoursAgo(): string { - return new Date(Date.now() - 2 * 60 * 60_000).toISOString(); -} - -function agentEntry(usedPct: number, fetchedAt = "2026-01-01T00:00:00.000Z"): UsageReportEntry { - return { - id: REPORT_ID, - account: { label: "Fixture account" }, - fetchedAt, - sourceId: "fixture", - sourceLabel: "Fixture plan", - icon: '', - report: { - status: "available", - planLabel: "Test plan", - windows: [ - { id: "session", label: "Session", usedPct: 3 }, - { id: "weekly", label: "Weekly", usedPct, headline: true }, - ], - }, - }; -} - -async function openMockAgent(page: Page) { - const session = await seedMockAgentWorkspace({ - repoPrefix: "usage-composer-pill-", - title: "Usage composer pill e2e", - initialPrompt: "emit 1 coalesced agent stream update for usage composer pill.", - }); - await openAgentRoute(page, session); - await expectComposerVisible(page); - return session; -} - -test.describe("usage composer pill", () => { - test("opens the cached report without a request and shows when it was fetched", async ({ - page, - }) => { - test.setTimeout(180_000); - const usage = await installUsageReportsFixture(page, { - agentReportIds: [REPORT_ID], - lists: [[agentEntry(42, twoHoursAgo())]], - }); - const session = await openMockAgent(page); - try { - const pill = page.getByTestId("usage-composer-pill"); - await expect(pill).toContainText("42%", { timeout: 30_000 }); - expect(usage.agentRequests()[0]).toEqual({ agentId: session.agentId }); - expect(usage.listRequests()).toEqual([{ forceRefresh: false, reportIds: [REPORT_ID] }]); - - await pill.click(); - const popover = page.getByTestId("usage-composer-popover"); - await expect(popover.getByText("Fixture plan", { exact: true })).toBeVisible(); - await expect(popover.getByText("Test plan")).toBeVisible(); - await expect(popover.getByText("42%")).toBeVisible(); - - await popover.getByTestId("usage-refresh").hover(); - await expect(page.getByTestId("usage-freshness-tooltip")).toHaveText("Updated 2h ago"); - await expect(popover.getByTestId("usage-freshness")).toHaveCount(0); - expect(usage.listRequests()).toHaveLength(1); - } finally { - await session.cleanup(); - } - }); - - test("refreshes only its report from the popover", async ({ page }) => { - test.setTimeout(180_000); - const usage = await installUsageReportsFixture(page, { - agentReportIds: [REPORT_ID], - lists: [[agentEntry(42, twoHoursAgo())], () => [agentEntry(64, new Date().toISOString())]], - }); - const session = await openMockAgent(page); - try { - const pill = page.getByTestId("usage-composer-pill"); - await expect(pill).toContainText("42%", { timeout: 30_000 }); - await pill.click(); - const popover = page.getByTestId("usage-composer-popover"); - await expect(popover.getByText("42%")).toBeVisible(); - - await popover.getByTestId("usage-refresh").click(); - await usage.waitForListRequests(2); - expect(usage.listRequests()).toEqual([ - { forceRefresh: false, reportIds: [REPORT_ID] }, - { forceRefresh: true, reportIds: [REPORT_ID] }, - ]); - await expect(popover.getByText("64%")).toBeVisible(); - await expect(pill).toContainText("64%"); - await expect(popover.getByTestId("usage-refresh")).toBeEnabled(); - - // Pressing closes the tooltip; it reopens on the next hover. - await popover.getByText("Fixture plan", { exact: true }).hover(); - await popover.getByTestId("usage-refresh").hover(); - await expect(page.getByTestId("usage-freshness-tooltip")).toHaveText("Updated just now"); - expect(usage.listRequests()).toHaveLength(2); - } finally { - await session.cleanup(); - } - }); - - test("keeps the previous report and says so when a refresh fails", async ({ page }) => { - test.setTimeout(180_000); - const usage = await installUsageReportsFixture(page, { - agentReportIds: [REPORT_ID], - lists: [ - [agentEntry(42, twoHoursAgo())], - { error: "Usage source timed out" }, - () => [agentEntry(64, new Date().toISOString())], - ], - }); - const session = await openMockAgent(page); - try { - const pill = page.getByTestId("usage-composer-pill"); - await expect(pill).toContainText("42%", { timeout: 30_000 }); - await pill.click(); - const popover = page.getByTestId("usage-composer-popover"); - const refresh = popover.getByTestId("usage-refresh"); - - await refresh.click(); - await usage.waitForListRequests(2); - await expect(popover.getByTestId("usage-refresh-error")).toHaveText( - "Unable to refresh usage", - ); - await expect(popover.getByText("42%")).toBeVisible(); - await expect(pill).toContainText("42%"); - - await refresh.click(); - await usage.waitForListRequests(3); - await expect(popover.getByText("64%")).toBeVisible(); - await expect(popover.getByTestId("usage-refresh-error")).toHaveCount(0); - } finally { - await session.cleanup(); - } - }); - - test("prints the freshness on the card where there is no hover", async ({ page }) => { - test.setTimeout(180_000); - await page.setViewportSize({ width: 390, height: 844 }); - const usage = await installUsageReportsFixture(page, { - agentReportIds: [REPORT_ID], - lists: [[agentEntry(42, twoHoursAgo())]], - }); - const session = await openMockAgent(page); - try { - const pill = page.getByTestId("usage-composer-pill"); - await expect(pill).toContainText("42%", { timeout: 30_000 }); - await pill.click(); - const popover = page.getByTestId("usage-composer-popover"); - await expect(popover.getByTestId("usage-freshness")).toHaveText("Updated 2h ago"); - expect(usage.listRequests()).toHaveLength(1); - } finally { - await session.cleanup(); - } - }); - - test("re-resolves the report ID after a turn and reuses the cached report", async ({ page }) => { - test.setTimeout(180_000); - const usage = await installUsageReportsFixture(page, { - agentReportIds: [REPORT_ID], - lists: [[agentEntry(42)]], - }); - const session = await openMockAgent(page); - try { - const pill = page.getByTestId("usage-composer-pill"); - await expect(pill).toContainText("42%", { timeout: 30_000 }); - const requestsBeforeTurn = usage.agentRequests().length; - const listRequestsBeforeTurn = usage.listRequests().length; - - await submitMessage(page, "emit 1 coalesced agent stream update for usage composer pill."); - - await usage.waitForAgentRequests(requestsBeforeTurn + 1); - expect(usage.agentRequests().at(-1)).toEqual({ - agentId: session.agentId, - }); - await expect(pill).toContainText("42%"); - expect(usage.listRequests()).toHaveLength(listRequestsBeforeTurn); - } finally { - await session.cleanup(); - } - }); - - test("is hidden when the agent has no usage report", async ({ page }) => { - test.setTimeout(180_000); - const usage = await installUsageReportsFixture(page, { agentReportIds: [null] }); - const session = await openMockAgent(page); - try { - await usage.waitForAgentRequests(1); - await expect(page.getByTestId("usage-composer-pill")).toHaveCount(0); - expect(usage.listRequests()).toHaveLength(0); - } finally { - await session.cleanup(); - } - }); - - test("is hidden on a host without usage sources", async ({ page }) => { - test.setTimeout(180_000); - const usage = await installUsageReportsFixture(page, { - usageSources: false, - agentReportIds: [REPORT_ID], - lists: [[agentEntry(42)]], - }); - const session = await openMockAgent(page); - try { - await expect(page.getByTestId("context-window-meter")).toBeVisible({ timeout: 30_000 }); - await expect(page.getByTestId("usage-composer-pill")).toHaveCount(0); - expect(usage.agentRequests()).toHaveLength(0); - } finally { - await session.cleanup(); - } - }); -}); diff --git a/packages/app/e2e/browser/usage-screen.spec.ts b/packages/app/e2e/browser/usage-screen.spec.ts index 70d2ae10b8..37e0ae26aa 100644 --- a/packages/app/e2e/browser/usage-screen.spec.ts +++ b/packages/app/e2e/browser/usage-screen.spec.ts @@ -1,3 +1,5 @@ +import path from "node:path"; +import type { Page } from "@playwright/test"; import type { UsageReportEntry } from "@getpaseo/protocol/messages"; import { expect, test } from "../support/fixtures"; import { gotoAppShell } from "../support/helpers/app"; @@ -9,8 +11,24 @@ function twoHoursAgo(): string { return new Date(Date.now() - 2 * 60 * 60_000).toISOString(); } +/** Set PASEO_QA_SCREENSHOT_DIR to keep a QA screenshot. */ +async function qaScreenshot(page: Page, name: string) { + const directory = process.env.PASEO_QA_SCREENSHOT_DIR; + if (!directory) return; + await page.waitForTimeout(600); + await page.addStyleTag({ content: ".__expo_fast_refresh { display: none !important; }" }); + await page.screenshot({ path: path.join(directory, `${name}.png`) }); +} + +/** The host selector names the host shown, even when it is the only one. */ +async function shownHostLabel(page: Page): Promise { + const selector = page.locator('[data-testid="usage-host-switcher"]:visible'); + await expect(selector).toHaveAccessibleName(/^Usage host: .+/, { timeout: 10_000 }); + return (await selector.innerText()).trim(); +} + test.describe("usage screen", () => { - test("opens from the sidebar and groups reports under their host", async ({ page }) => { + test("opens from the sidebar on the host's reports, naming the one host", async ({ page }) => { test.setTimeout(120_000); const serverId = getServerId(); const usage = await installUsageReportsFixture(page, { @@ -24,7 +42,7 @@ test.describe("usage screen", () => { sourceLabel: "Alpha plan", report: { status: "available", - windows: [{ id: "weekly", label: "Weekly", usedPct: 31, headline: true }], + windows: [{ id: "weekly", label: "Weekly", usedPct: 31 }], }, }, { @@ -46,6 +64,11 @@ test.describe("usage screen", () => { const group = page.getByTestId(`usage-host-${serverId}`); await expect(group.getByText("Alpha plan", { exact: true })).toBeVisible({ timeout: 10_000 }); + const hostLabel = await shownHostLabel(page); + await expect(page.locator('[data-testid="usage-host-switcher"]:visible')).toHaveAccessibleName( + `Usage host: ${hostLabel}`, + ); + await qaScreenshot(page, "phase7-usage-screen-one-host"); await expect(group.getByText("31%")).toBeVisible(); await expect(group.getByText("Beta plan", { exact: true })).toBeVisible(); await expect(group.getByText("Unavailable", { exact: true })).toBeVisible(); @@ -66,7 +89,7 @@ test.describe("usage screen", () => { sourceLabel: "Beta plan", report: { status: "available", - windows: [{ id: "weekly", label: "Weekly", usedPct: 12, headline: true }], + windows: [{ id: "weekly", label: "Weekly", usedPct: 12 }], }, }; const alpha = (usedPct: number, fetchedAt: string): UsageReportEntry => ({ @@ -77,7 +100,7 @@ test.describe("usage screen", () => { sourceLabel: "Alpha plan", report: { status: "available", - windows: [{ id: "weekly", label: "Weekly", usedPct, headline: true }], + windows: [{ id: "weekly", label: "Weekly", usedPct }], }, }); const usage = await installUsageReportsFixture(page, { @@ -138,19 +161,21 @@ test.describe("usage screen", () => { await expect(group.getByTestId("usage-freshness")).toHaveText("Updated 2h ago"); }); - test("tells the user to update a host without usage sources", async ({ page }) => { + test("tells the user to update a host without usage support", async ({ page }) => { test.setTimeout(120_000); const serverId = getServerId(); - const usage = await installUsageReportsFixture(page, { usageSources: false }); + const usage = await installUsageReportsFixture(page, { usageSupported: false }); await gotoAppShell(page); await page.locator('[data-testid="sidebar-usage"]:visible').first().click(); + const hostLabel = await shownHostLabel(page); await expect( - page.getByTestId(`usage-host-${serverId}`).getByText("Update the host to see usage", { + page.getByTestId(`usage-host-${serverId}`).getByText(`Update ${hostLabel} to see usage`, { exact: true, }), ).toBeVisible({ timeout: 10_000 }); + await qaScreenshot(page, "phase7-usage-update-host"); expect(usage.listRequests()).toHaveLength(0); }); }); diff --git a/packages/app/e2e/browser/usage-settings.spec.ts b/packages/app/e2e/browser/usage-settings.spec.ts index 623bc51e67..d99b93fdd2 100644 --- a/packages/app/e2e/browser/usage-settings.spec.ts +++ b/packages/app/e2e/browser/usage-settings.spec.ts @@ -39,7 +39,7 @@ test.describe("usage settings", () => { sourceLabel: "Alpha plan", report: { planLabel: "Max", - windows: [{ id: "session", label: "Session", usedPct: 7, headline: true }], + windows: [{ id: "session", label: "Session", usedPct: 7 }], }, }), report({ @@ -82,6 +82,10 @@ test.describe("usage settings", () => { await expect(card.getByText("$5.00 / $20.00", { exact: true })).toBeVisible(); await expect(card.getByText("2026-12-31", { exact: true })).toBeVisible(); await expect(card.getByText("Gamma auth expired", { exact: true })).toBeVisible(); + + await card.getByTestId("usage-display-remaining").click(); + await expect(card.getByText("30% left")).toBeVisible(); + await expect(card.getByText("93% left")).toBeVisible(); }); test("refresh forces a fresh report", async ({ page }) => { @@ -119,17 +123,18 @@ test.describe("usage settings", () => { await expect(page.getByText("64%")).toBeVisible(); }); - test("asks to update a host without usage sources and never calls it", async ({ page }) => { + test("asks to update a host without usage support and never calls it", async ({ page }) => { test.setTimeout(120_000); const serverId = getServerId(); - const usage = await installUsageReportsFixture(page, { usageSources: false }); + const usage = await installUsageReportsFixture(page, { usageSupported: false }); await gotoAppShell(page); await openSettings(page); await openSettingsHostSection(page, serverId, "usage"); await expect( - page.getByTestId("usage-card").getByText("Update the host to see usage", { exact: true }), + // Names the host: "Update Laptop to see usage". + page.getByTestId("usage-card").getByText(/^Update (?!the host ).+ to see usage$/), ).toBeVisible({ timeout: 10_000 }); expect(usage.listRequests()).toHaveLength(0); }); diff --git a/packages/app/e2e/browser/usage-sidebar-item.spec.ts b/packages/app/e2e/browser/usage-sidebar-item.spec.ts new file mode 100644 index 0000000000..4fdd06d62b --- /dev/null +++ b/packages/app/e2e/browser/usage-sidebar-item.spec.ts @@ -0,0 +1,192 @@ +import path from "node:path"; +import type { Locator, Page } from "@playwright/test"; +import { expect, test } from "../support/fixtures"; +import { gotoAppShell, openSettings } from "../support/helpers/app"; +import { getServerId } from "../support/helpers/server-id"; +import { openSettingsHostSection } from "../support/helpers/settings"; +import { leaveSettings, openSidebarNavSettings } from "../support/helpers/sidebar-nav-settings"; +import { installUsageReportsFixture } from "../support/helpers/usage-reports"; +import { + claudeAndCodexReports, + expectNoPinnedUsage, + expectOnUsageScreen, + expectPinnedUsage, + leaveUsageScreen, + openCompactSidebar, + pinRow, + showUsageAs, + togglePin, + usageItem, + usageSheet, +} from "../support/helpers/usage-sidebar-item"; + +const WIDE = { width: 1440, height: 900 }; +const COMPACT = { width: 390, height: 844 }; + +type ScreenshotArea = { kind: "page" } | { kind: "footer" } | { kind: "element"; locator: Locator }; + +/** Set PASEO_QA_SCREENSHOT_DIR to keep QA screenshots of each state. */ +async function qaScreenshot(page: Page, name: string, area: ScreenshotArea = { kind: "page" }) { + const directory = process.env.PASEO_QA_SCREENSHOT_DIR; + if (!directory) return; + // Let sheet and drawer animations settle so the image shows the final state. + await page.waitForTimeout(600); + // Expo's fast-refresh indicator sits over the footer's Hosts icon. + await page.addStyleTag({ content: ".__expo_fast_refresh { display: none !important; }" }); + const file = path.join(directory, `phase4-${name}.png`); + if (area.kind === "element") { + await area.locator.screenshot({ path: file }); + return; + } + const clip = area.kind === "footer" ? await footerClip(page) : undefined; + await page.screenshot({ path: file, clip }); +} + +/** The sidebar footer, from Add project down to the icon row, with some margin. */ +async function footerClip(page: Page) { + const top = (await page.locator('[data-testid="sidebar-add-project"]:visible').boundingBox())!; + const bottom = (await page.locator('[data-testid="sidebar-settings"]:visible').boundingBox())!; + const margin = 16; + const x = Math.max(0, top.x - margin); + const y = Math.max(0, top.y - margin); + return { x, y, width: 300, height: bottom.y + bottom.height + margin - y }; +} + +test.describe("Usage item", () => { + test("pinned windows show in the sidebar, follow the used/remaining toggle and persist", async ({ + page, + }) => { + test.setTimeout(180_000); + const serverId = getServerId(); + await installUsageReportsFixture(page, { lists: [() => claudeAndCodexReports()] }); + await page.setViewportSize(WIDE); + await gotoAppShell(page); + const screen = page.getByTestId(`usage-host-${serverId}`); + + await test.step("a fresh device shows the plain Usage row, which opens the Usage screen", async () => { + await expect(usageItem(page)).toBeVisible({ timeout: 30_000 }); + await expectNoPinnedUsage(page); + await qaScreenshot(page, "desktop-footer-empty", { kind: "footer" }); + await usageItem(page).click(); + await expectOnUsageScreen(page); + }); + + await test.step("pinning Claude 5-hour and Codex weekly shows both in the Usage item", async () => { + await expect(screen.getByText("Claude", { exact: true })).toBeVisible({ timeout: 10_000 }); + // One host: the selector still names it. + await expect( + page.locator('[data-testid="usage-host-switcher"]:visible'), + ).toHaveAccessibleName(/^Usage host: .+/); + await togglePin(screen, "Claude", "Session"); + await togglePin(screen, "Codex", "Weekly"); + await expectPinnedUsage(page, ["31%", "12%"]); + await expect(usageItem(page)).not.toHaveText("Usage"); + await qaScreenshot(page, "desktop-footer-pins", { kind: "footer" }); + await expect(page.getByRole("button", { name: "Refresh", exact: true })).toBeVisible(); + await qaScreenshot(page, "usage-screen-header", { + kind: "element", + locator: page.getByTestId("page-title").locator(".."), + }); + await qaScreenshot(page, "usage-card-refresh", { + kind: "element", + locator: screen.getByTestId("usage-report-claude:default"), + }); + }); + + await test.step("remaining flips the Usage item and the Usage screen", async () => { + await showUsageAs(page, "remaining"); + await expectPinnedUsage(page, ["69% left", "88% left"]); + await expect( + screen.getByTestId("usage-report-claude:default").getByText("69% left"), + ).toBeVisible(); + await expect( + screen.getByTestId("usage-report-codex:default").getByText("88% left"), + ).toBeVisible(); + }); + + await test.step("on desktop the Usage item opens the Usage screen", async () => { + await gotoAppShell(page); + await expect(page).not.toHaveURL(/\/usage$/); + await usageItem(page).click(); + await expectOnUsageScreen(page); + await expect(page.getByTestId("usage-expanded")).toHaveCount(0); + }); + + await test.step("on a phone the Usage item opens a bottom sheet", async () => { + await page.setViewportSize(COMPACT); + await leaveUsageScreen(page); + await openCompactSidebar(page); + await expect(usageItem(page)).toBeInViewport(); + await expectPinnedUsage(page, ["69% left", "88% left"]); + await qaScreenshot(page, "compact-footer"); + await usageItem(page).click(); + const sheet = usageSheet(page); + await expect(sheet.getByText("Codex", { exact: true })).toBeInViewport(); + await expect(sheet.getByText("88% left")).toBeVisible(); + await expect(pinRow(sheet, "Claude", "Session")).toBeChecked(); + await expect(pinRow(sheet, "Claude", "Weekly")).not.toBeChecked(); + // The row reads its window, percent and reset; the checkbox state says it is pinned. + await expect(pinRow(sheet, "Claude", "Session")).toHaveAccessibleName( + /^Pin Claude Session, \d+% left( · .+)?$/, + ); + // The sheet carries the Usage screen's controls, host selector included. + await expect( + page.locator('[data-testid="usage-host-switcher"]:visible'), + ).toHaveAccessibleName(/^Usage host: .+/); + await expect(page.locator('[data-testid="usage-refresh-all"]:visible')).toBeVisible(); + await expect(page).not.toHaveURL(/\/usage$/); + const sheetBox = (await sheet.boundingBox())!; + expect(sheetBox.y).toBeGreaterThan(COMPACT.height / 3); + expect(sheetBox.width).toBeGreaterThan(COMPACT.width * 0.8); + await qaScreenshot(page, "compact-sheet"); + await qaScreenshot(page, "phase7-compact-sheet"); + // Tap the backdrop above the sheet. + await page.mouse.click(COMPACT.width / 2, sheetBox.y / 2); + await expect(sheet).toHaveCount(0); + await page.setViewportSize(WIDE); + await usageItem(page).click(); + await expectOnUsageScreen(page); + }); + + await test.step("a reload keeps the pins and the toggle", async () => { + await page.reload(); + await expectPinnedUsage(page, ["69% left", "88% left"]); + await expect( + page.locator('[data-testid="usage-display-remaining"]:visible').first(), + ).toHaveAttribute("aria-selected", "true"); + }); + + await test.step("the Settings usage section shares the pins and the toggle", async () => { + const usageUrl = page.url(); + await openSettings(page); + await openSettingsHostSection(page, serverId, "usage"); + const section = page.getByTestId("usage-card"); + await expect(section.getByText("69% left")).toBeVisible({ timeout: 10_000 }); + await expect(pinRow(section, "Codex", "Weekly")).toBeChecked(); + await page.goto(usageUrl); + await expect(screen.getByText("Claude", { exact: true })).toBeVisible({ timeout: 10_000 }); + }); + + await test.step("unpinning both brings back the Usage row, and that survives a reload", async () => { + await togglePin(screen, "Claude", "Session"); + await togglePin(screen, "Codex", "Weekly"); + await expectNoPinnedUsage(page); + await page.reload(); + await expect(screen.getByText("88% left")).toBeVisible({ timeout: 10_000 }); + await expectNoPinnedUsage(page); + await gotoAppShell(page); + await usageItem(page).click(); + await expectOnUsageScreen(page); + }); + + await test.step("Settings > Sidebar lists the Usage item", async () => { + await openSidebarNavSettings(page); + const footer = page.getByTestId("sidebar-nav-section-footer"); + await expect( + footer.getByTestId("sidebar-nav-item-usage").getByText("Usage", { exact: true }), + ).toBeVisible(); + await qaScreenshot(page, "settings-sidebar-footer", { kind: "element", locator: footer }); + await leaveSettings(page); + }); + }); +}); diff --git a/packages/app/e2e/support/helpers/plugin-buttons.ts b/packages/app/e2e/support/helpers/plugin-buttons.ts index 15e5c8bc37..7404173fc8 100644 --- a/packages/app/e2e/support/helpers/plugin-buttons.ts +++ b/packages/app/e2e/support/helpers/plugin-buttons.ts @@ -34,7 +34,7 @@ function Details({ workspaceId, theme, layout, close }) { React.useEffect(() => { const owner = paseo.observeEvents(["project.update"]); owner.subscribe({ snapshot(value) { setOwnerId(value.subscriptionId); }, update() { setUpdates((n) => n + 1); } }); - // Deliberately leave this observation to the mounted host scope. + return () => { void owner.release(); }; }, [paseo]); const rpc = useRpc(summary); const query = useQuery({ queryKey: ["summary"], queryFn: () => rpc({}) }); diff --git a/packages/app/e2e/support/helpers/plugin-sidebar-items.ts b/packages/app/e2e/support/helpers/plugin-sidebar-items.ts new file mode 100644 index 0000000000..0722bd8317 --- /dev/null +++ b/packages/app/e2e/support/helpers/plugin-sidebar-items.ts @@ -0,0 +1,268 @@ +import { mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { expect, type Locator, type Page } from "@playwright/test"; +import { openCommandCenter } from "./command-center"; +import { connectNewWorkspaceDaemonClient } from "./new-workspace"; +import { pluginRequirements } from "./plugin-fixture"; + +export const SHOWCASE_PLUGIN_ID = "sidebar-showcase"; +export const LEGACY_PLUGIN_ID = "legacy-sidebar"; +export const BOTS_PLUGIN_ID = "sidebar-bots"; +export const WIDE = { width: 1440, height: 900 }; +export const COMPACT = { width: 390, height: 844 }; + +/** A plugin written against the current API: a screen, header and footer items, a command. */ +const SHOWCASE_SOURCE = `import React from "react"; +import { Pressable, Text, View } from "react-native"; +import { SidebarRow } from "@getpaseo/plugin/client/ui"; + +function DeploysScreen({ theme }) { + return Deploys screen body; +} + +function RefreshButton({ theme, onPress }) { + return ( + + Refresh + + ); +} + +function DeploysItem({ theme, currentScreen, openScreen }) { + const [refreshes, setRefreshes] = React.useState(0); + return ( + openScreen({ screenId: "deploys" })} + trailing={ setRefreshes((count) => count + 1)} />} + /> + ); +} + +function SyncDetails({ theme, layout, close, openScreen }) { + return ( + + Sync details + Presentation: {layout.compact ? "compact" : "wide"} + openScreen({ screenId: "deploys" })}>Open deploys + Done + + ); +} + +function SyncItem({ openPopover }) { + return openPopover(SyncDetails)} />; +} + +function Broken() { + throw new Error("Sidebar item exploded"); +} + +export default function contribute(client) { + client.addScreen({ id: "deploys", title: "Deploys", Component: DeploysScreen }); + client.addSidebarHeaderItem({ id: "deploys", title: "Deploys", Component: DeploysItem }); + client.addSidebarHeaderItem({ id: "broken", title: "Broken header", Component: Broken }); + client.addSidebarFooterItem({ id: "sync", title: "Sync", Component: SyncItem }); + client.addSidebarFooterItem({ id: "broken", title: "Broken footer", Component: Broken }); + client.addCommandCenterItem({ id: "open-deploys", title: "Open deploys", icon: "Rocket", context: "global", onSelect: (ctx) => ctx.openScreen({ screenId: "deploys" }) }); + return () => {}; +} +`; + +/** + * One header item that renders a group: a row per bot, a separator and a status row. Each bot + * row opens the bot screen with the bot's id as a param, and its trailing More button opens that + * bot's popover. Commands open a bot with a `serverId` param, and add and remove a footer item + * after setup; that item opens an event observation, releases it in its cleanup, and publishes its + * id on `globalThis.__botAlertsObservation`. + */ +const BOTS_SOURCE = `import React from "react"; +import { Pressable, Text, View } from "react-native"; +import { usePaseo } from "@getpaseo/plugin/client"; +import { SidebarRow, SidebarSeparator } from "@getpaseo/plugin/client/ui"; + +const BOTS = [ + { id: "bot-1", name: "Bot 1" }, + { id: "bot-2", name: "Bot 2" }, +]; + +function BotScreen({ theme, params }) { + return ( + + {"Bot screen: " + params.botId} + {"serverId param: " + (params.serverId ?? "none")} + + ); +} + +function botTitle(params) { + return BOTS.find((bot) => bot.id === params.botId)?.name ?? "Bot"; +} + +const botMenus = {}; + +function botMenu(bot) { + botMenus[bot.id] ??= function BotMenu({ theme, close }) { + return ( + + {bot.name + " options"} + Done + + ); + }; + return botMenus[bot.id]; +} + +function MoreButton({ theme, bot, onPress }) { + return ( + + More + + ); +} + +function BotStatus({ theme, close }) { + return ( + + Bot status details + Done + + ); +} + +function BotsItem({ theme, currentScreen, openScreen, openPopover }) { + return ( + <> + {BOTS.map((bot) => ( + openScreen({ screenId: "bot", params: { botId: bot.id } })} + trailing={ openPopover(botMenu(bot))} />} + /> + ))} + + openPopover(BotStatus)} /> + + ); +} + +function AlertsItem() { + const paseo = usePaseo(); + React.useEffect(() => { + const observation = paseo.observeEvents(["project.update"]); + observation.subscribe({ snapshot(value) { globalThis.__botAlertsObservation = value.subscriptionId; }, update() {} }); + return () => { void observation.release(); }; + }, [paseo]); + return {}} />; +} + +export default function contribute(client) { + let removeAlerts = null; + client.addScreen({ id: "bot", title: botTitle, Component: BotScreen }); + client.addSidebarHeaderItem({ id: "bots", title: "Bots", Component: BotsItem }); + client.addCommandCenterItem({ id: "open-bot-3", title: "Open bot 3 on server x", icon: "Bot", context: "global", onSelect: (ctx) => ctx.openScreen({ screenId: "bot", params: { botId: "bot-3", serverId: "x" } }) }); + client.addCommandCenterItem({ id: "add-alerts", title: "Add bot alerts", icon: "Bell", context: "global", onSelect: () => { + if (!removeAlerts) removeAlerts = client.addSidebarFooterItem({ id: "alerts", title: "Bot alerts", Component: AlertsItem }); + } }); + client.addCommandCenterItem({ id: "remove-alerts", title: "Remove bot alerts", icon: "BellOff", context: "global", onSelect: () => { + removeAlerts?.(); + removeAlerts = null; + } }); + return () => {}; +} +`; + +/** A plugin that still uses only the old descriptor calls. */ +const LEGACY_SOURCE = `import React from "react"; +import { Text, View } from "react-native"; + +function Main({ theme }) { + return Legacy screen body; +} + +export default function contribute(plugin) { + plugin.addSurface("main", Main); + plugin.addSidebarItem({ id: "entry", title: "Legacy entry", icon: "Server", surface: "main" }); + plugin.addSidebarItem({ id: "hidden", title: "Legacy hidden", icon: "Blocks", surface: "main" }); + return () => {}; +} +`; + +export async function installSidebarPlugins() { + const client = await connectNewWorkspaceDaemonClient({ ownProjects: false }); + const config = await client.getDaemonConfig(); + const directories: string[] = []; + for (const [id, source] of [ + [SHOWCASE_PLUGIN_ID, SHOWCASE_SOURCE], + [LEGACY_PLUGIN_ID, LEGACY_SOURCE], + [BOTS_PLUGIN_ID, BOTS_SOURCE], + ] as const) { + const directory = await mkdtemp(path.join(tmpdir(), `paseo-${id}-`)); + directories.push(directory); + await writeFile( + path.join(directory, "paseo-plugin.json"), + JSON.stringify({ id, requirements: pluginRequirements }), + ); + await writeFile(path.join(directory, "index.client.tsx"), source); + } + await client.patchDaemonConfig({ pluginsEnabled: true }); + for (const directory of directories) await client.installDirectoryPlugin(directory); + return { + async cleanup() { + await client.removePlugin(SHOWCASE_PLUGIN_ID); + await client.removePlugin(LEGACY_PLUGIN_ID); + await client.removePlugin(BOTS_PLUGIN_ID); + await client.patchDaemonConfig({ pluginsEnabled: config.config.pluginsEnabled }); + await client.close(); + await Promise.all( + directories.map((directory) => rm(directory, { recursive: true, force: true })), + ); + }, + }; +} + +/** The first visible match: the shell keeps a compact copy of the sidebar mounted. */ +export function visibleTestId(page: Page, testID: string): Locator { + return page.locator(`[data-testid="${testID}"]:visible`).first(); +} + +export function headerRow(page: Page, pluginId: string, itemId: string): Locator { + return visibleTestId(page, `plugin-sidebar-${pluginId}-${itemId}`); +} + +export function footerItem(page: Page, pluginId: string, itemId: string): Locator { + return visibleTestId(page, `plugin-sidebar-footer-${pluginId}-${itemId}`); +} + +export async function expectRowActive(row: Locator, active: boolean): Promise { + if (active) await expect(row).toHaveAttribute("aria-selected", "true"); + else await expect(row).not.toHaveAttribute("aria-selected", "true"); +} + +export async function runCommand(page: Page, title: string): Promise { + const panel = await openCommandCenter(page); + await panel.getByTestId("command-center-input").fill(title); + await panel.getByRole("button", { name: title, exact: true }).click(); + await expect(panel).not.toBeVisible(); +} + +export async function closeScreen(page: Page): Promise { + await page.getByTestId("plugin-surface-close").click(); + await expect(page).not.toHaveURL(/\/plugin\//); +} + +/** Box of the popover body, located from the text it renders. */ +export async function popoverBox(page: Page, text = "Sync details") { + const details = page.getByText(text, { exact: true }); + await expect(details).toBeInViewport(); + const box = await details.boundingBox(); + expect(box).not.toBeNull(); + return box!; +} diff --git a/packages/app/e2e/support/helpers/sidebar-nav-settings.ts b/packages/app/e2e/support/helpers/sidebar-nav-settings.ts index b5f1a77683..f57ee32497 100644 --- a/packages/app/e2e/support/helpers/sidebar-nav-settings.ts +++ b/packages/app/e2e/support/helpers/sidebar-nav-settings.ts @@ -55,10 +55,25 @@ export async function seedSidebarNavPreferences( ); } +/** Seeds stored footer rows once; a reload keeps whatever the app wrote since. */ +export async function seedSidebarFooterPreferences( + page: Page, + preferences: SidebarNavPreference[], +): Promise { + await page.addInitScript( + ({ key, sidebarFooterItems }) => { + if (localStorage.getItem(key)) return; + localStorage.setItem(key, JSON.stringify({ sidebarFooterItems })); + }, + { key: APP_SETTINGS_KEY, sidebarFooterItems: preferences }, + ); +} + export async function openSidebarNavSettings(page: Page): Promise { await openSettings(page); await openSettingsSection(page, "sidebar"); - await expect(page.getByTestId("sidebar-nav-section")).toBeVisible({ timeout: 30_000 }); + await expect(page.getByTestId("sidebar-nav-section-header")).toBeVisible({ timeout: 30_000 }); + await expect(page.getByTestId("sidebar-nav-section-footer")).toBeVisible(); } export async function leaveSettings(page: Page): Promise { @@ -125,9 +140,9 @@ export async function expectStoredSidebarNav( .toEqual(expected); } -async function expectVerticalOrder( - keys: SidebarNavKey[], - locate: (key: SidebarNavKey) => Locator, +async function expectVerticalOrder( + keys: Key[], + locate: (key: Key) => Locator, subject: string, ): Promise { await expect @@ -136,11 +151,7 @@ async function expectVerticalOrder( const measured = await Promise.all( keys.map(async (key) => ({ key, top: await rowTop(locate(key)) })), ); - if ( - !measured.every( - (entry): entry is { key: SidebarNavKey; top: number } => entry.top !== null, - ) - ) + if (!measured.every((entry): entry is { key: Key; top: number } => entry.top !== null)) return null; return measured.sort((a, b) => a.top - b.top).map((entry) => entry.key); }, @@ -148,3 +159,83 @@ async function expectVerticalOrder( ) .toEqual(keys); } + +/** Persisted footer row key -> the testID the app shell renders that row with. */ +function shellFooterTestID(key: string): string { + // With nothing pinned the Usage item is the plain Usage row. + if (key === "usage") return "sidebar-usage"; + const [, pluginId, itemId] = key.split(":"); + return `plugin-sidebar-footer-${pluginId}-${itemId}`; +} + +function shellFooterRow(page: Page, key: string): Locator { + return page.locator(`[data-testid="${shellFooterTestID(key)}"]:visible`).first(); +} + +function footerSettingsRows(page: Page): Locator { + return page + .getByTestId("sidebar-nav-section-footer") + .locator('[data-testid^="sidebar-nav-item-"]'); +} + +export async function expectFooterSettingsKeys(page: Page, keys: string[]): Promise { + await expect + .poll(async () => + ( + await footerSettingsRows(page).evaluateAll((rows) => + rows.map((row) => row.getAttribute("data-testid")), + ) + ).map((testID) => testID?.replace("sidebar-nav-item-", "")), + ) + .toEqual(keys); +} + +export async function moveFooterItemUp(page: Page, key: string): Promise { + await page + .getByTestId("sidebar-nav-section-footer") + .getByTestId(`sidebar-nav-move-up-${key}`) + .click(); +} + +export async function setFooterItemVisible( + page: Page, + key: string, + visible: boolean, +): Promise { + const toggle = page + .getByTestId("sidebar-nav-section-footer") + .getByTestId(`sidebar-nav-toggle-${key}`); + await toggle.click(); + await expect(toggle).toHaveAttribute("aria-checked", String(visible)); +} + +export async function expectFooterOrder(page: Page, keys: string[]): Promise { + await expectVerticalOrder(keys, (key) => shellFooterRow(page, key), "sidebar footer rows"); +} + +export async function expectFooterItemHidden(page: Page, key: string): Promise { + await expect(page.locator(`[data-testid="${shellFooterTestID(key)}"]:visible`)).toHaveCount(0); +} + +const FOOTER_ICON_TEST_IDS = [ + "sidebar-hosts-trigger", + "sidebar-import-session", + "sidebar-help", + "sidebar-settings", +]; + +/** The fixed icon row: Hosts, Import session, Help and support, Settings, left to right. */ +export async function expectFooterIconRow(page: Page): Promise { + const boxes = await Promise.all( + FOOTER_ICON_TEST_IDS.map((testID) => + page.locator(`[data-testid="${testID}"]:visible`).first().boundingBox(), + ), + ); + const [first, ...rest] = boxes.map((box) => box!); + let previous = first; + for (const box of rest) { + expect(Math.abs(box.y - first.y)).toBeLessThan(2); + expect(box.x).toBeGreaterThan(previous.x); + previous = box; + } +} diff --git a/packages/app/e2e/support/helpers/usage-reports.ts b/packages/app/e2e/support/helpers/usage-reports.ts index 6a43d0fe30..339475b8e6 100644 --- a/packages/app/e2e/support/helpers/usage-reports.ts +++ b/packages/app/e2e/support/helpers/usage-reports.ts @@ -4,9 +4,7 @@ import { daemonWsRoutePattern } from "./daemon-port"; export interface UsageReportsFixture { listRequests(): Array<{ forceRefresh: boolean; reportIds?: string[] }>; - agentRequests(): Array<{ agentId: string }>; waitForListRequests(count: number): Promise; - waitForAgentRequests(count: number): Promise; } interface UsageReportsFixtureOptions { @@ -15,10 +13,8 @@ interface UsageReportsFixtureOptions { * request; a function builds the response when the request arrives (e.g. a fresh `fetchedAt`). */ lists?: Array UsageListResponse)>; - /** Successive agent report IDs; the last one repeats. */ - agentReportIds?: Array; - /** Advertise `features.usageSources`. False simulates a host from before usage sources. */ - usageSources?: boolean; + /** False simulates a host with no usage reporting capability. */ + usageSupported?: boolean; } type UsageListResponse = UsageReportEntry[] | { error: string }; @@ -42,7 +38,7 @@ function getSessionMessage(message: WebSocketMessage): Record | return envelope.message as Record; } -function withUsageSourcesFeature(message: WebSocketMessage, enabled: boolean): string | null { +function withUsageSupportFeature(message: WebSocketMessage, enabled: boolean): string | null { const envelope = parseJson(message) as { type?: unknown; message?: { type?: unknown; payload?: Record }; @@ -61,7 +57,10 @@ function withUsageSourcesFeature(message: WebSocketMessage, enabled: boolean): s ...envelope, message: { ...envelope.message, - payload: { ...payload, features: { ...features, usageSources: enabled } }, + payload: { + ...payload, + features: { ...features, usageSources: enabled, providerUsageList: enabled }, + }, }, }); } @@ -95,10 +94,8 @@ export async function installUsageReportsFixture( options: UsageReportsFixtureOptions, ): Promise { const listRequests: Array<{ forceRefresh: boolean; reportIds?: string[] }> = []; - const agentRequests: Array<{ agentId: string }> = []; const listCounter = createCounter(); - const agentCounter = createCounter(); - const usageSources = options.usageSources ?? true; + const usageSupported = options.usageSupported ?? true; await page.routeWebSocket(daemonWsRoutePattern(), (ws) => { const server = ws.connectToServer(); @@ -142,37 +139,18 @@ export async function installUsageReportsFixture( listCounter.increment(); return; } - if (request?.type === "agent.resolve_usage_report.request" && typeof requestId === "string") { - agentRequests.push({ - agentId: String(request.agentId), - }); - const reportId = pick(options.agentReportIds ?? [null], agentRequests.length - 1); - ws.send( - JSON.stringify({ - type: "session", - message: { - type: "agent.resolve_usage_report.response", - payload: { requestId, reportId }, - }, - }), - ); - agentCounter.increment(); - return; - } server.send(message); }); server.onMessage((message) => { const serverInfo = - typeof message === "string" ? withUsageSourcesFeature(message, usageSources) : null; + typeof message === "string" ? withUsageSupportFeature(message, usageSupported) : null; ws.send(serverInfo ?? message); }); }); return { listRequests: () => [...listRequests], - agentRequests: () => [...agentRequests], waitForListRequests: (count) => listCounter.waitFor(count), - waitForAgentRequests: (count) => agentCounter.waitFor(count), }; } diff --git a/packages/app/e2e/support/helpers/usage-sidebar-item.ts b/packages/app/e2e/support/helpers/usage-sidebar-item.ts new file mode 100644 index 0000000000..342045cd1d --- /dev/null +++ b/packages/app/e2e/support/helpers/usage-sidebar-item.ts @@ -0,0 +1,111 @@ +import { readFileSync } from "node:fs"; +import path from "node:path"; +import type { UsageReportEntry } from "@getpaseo/protocol/messages"; +import { expect, type Locator, type Page } from "@playwright/test"; + +const PLUGINS_DIR = path.resolve(__dirname, "../../../../../plugins"); + +function sourceIcon(plugin: string): string { + return readFileSync(path.join(PLUGINS_DIR, plugin, "icon.svg"), "utf8"); +} + +function inOneDay(): string { + return new Date(Date.now() + 24 * 60 * 60_000).toISOString(); +} + +/** Claude and Codex reports shaped like the built-in sources report them. */ +export function claudeAndCodexReports(): UsageReportEntry[] { + const fetchedAt = new Date().toISOString(); + return [ + { + id: "claude:default", + account: { label: "dev@example.com" }, + fetchedAt, + sourceId: "claude", + sourceLabel: "Claude", + icon: sourceIcon("claude-usage-source"), + report: { + status: "available", + planLabel: "Max", + windows: [ + { id: "five_hour", label: "Session", usedPct: 31, resetsAt: inOneDay() }, + { id: "weekly", label: "Weekly", usedPct: 54, resetsAt: inOneDay() }, + ], + }, + }, + { + id: "codex:default", + account: { label: "dev@example.com" }, + fetchedAt, + sourceId: "codex", + sourceLabel: "Codex", + icon: sourceIcon("codex-usage-source"), + report: { + status: "available", + planLabel: "Pro", + windows: [ + { id: "session", label: "Session", usedPct: 7, resetsAt: inOneDay() }, + { id: "weekly", label: "Weekly", usedPct: 12, resetsAt: inOneDay() }, + ], + }, + }, + ]; +} + +/** The first visible match: the shell keeps a compact copy of the sidebar mounted. */ +function visible(page: Page, testID: string): Locator { + return page.locator(`[data-testid="${testID}"]:visible`).first(); +} + +/** The sidebar footer's Usage item: pinned windows, or a plain "Usage" row without any. */ +export function usageItem(page: Page): Locator { + return visible(page, "sidebar-usage"); +} + +/** The compact usage sheet the Usage item opens. */ +export function usageSheet(page: Page): Locator { + return visible(page, "usage-expanded"); +} + +export async function expectOnUsageScreen(page: Page): Promise { + await expect(page).toHaveURL(/\/usage$/); +} + +export async function expectPinnedUsage(page: Page, percents: string[]): Promise { + const windows = usageItem(page).getByTestId("sidebar-usage-pinned-window"); + await expect(windows).toHaveText(percents); +} + +/** Without pinned windows the Usage item is a plain row that reads "Usage". */ +export async function expectNoPinnedUsage(page: Page): Promise { + await expect(page.locator('[data-testid="sidebar-usage-pinned-window"]:visible')).toHaveCount(0); + await expect(usageItem(page)).toHaveText("Usage"); +} + +/** A window row, which is itself the pin toggle: "Claude", "Session". */ +export function pinRow(scope: Locator, source: string, window: string): Locator { + // The row's label goes on with its percent and reset: "Pin Claude Session, 31% · resets in 2h". + return scope.getByRole("checkbox", { name: new RegExp(`^Pin ${source} ${window}, `) }); +} + +export async function togglePin(scope: Locator, source: string, window: string) { + const row = pinRow(scope, source, window); + const pinned = await row.isChecked(); + await row.click(); + await expect(row).toBeChecked({ checked: !pinned }); +} + +export async function showUsageAs(scope: Locator | Page, displayAs: "used" | "remaining") { + await scope.locator(`[data-testid="usage-display-${displayAs}"]:visible`).first().click(); +} + +/** Opens the compact sidebar drawer. */ +export async function openCompactSidebar(page: Page): Promise { + await page.getByRole("button", { name: "Open menu", exact: true }).first().click(); +} + +/** On a phone the Usage screen has a back header instead of the sidebar menu. */ +export async function leaveUsageScreen(page: Page): Promise { + await page.getByRole("button", { name: "Back", exact: true }).first().click(); + await expect(page).not.toHaveURL(/\/usage$/); +} diff --git a/packages/app/src/agent-stream/presentation.test.ts b/packages/app/src/agent-stream/presentation.test.ts index 5e625a9028..54160ebcb9 100644 --- a/packages/app/src/agent-stream/presentation.test.ts +++ b/packages/app/src/agent-stream/presentation.test.ts @@ -62,6 +62,8 @@ function installProbe( clientBundle, lifetime: new AbortController(), queryClient: new QueryClient(), + paseo: {} as InstalledPlugin["paseo"], + invoke: async () => undefined, }; } diff --git a/packages/app/src/components/hosts/host-switcher.tsx b/packages/app/src/components/hosts/host-switcher.tsx new file mode 100644 index 0000000000..9172ab3e5d --- /dev/null +++ b/packages/app/src/components/hosts/host-switcher.tsx @@ -0,0 +1,83 @@ +import { ChevronDown } from "lucide-react-native"; +import { useCallback, useRef, useState } from "react"; +import { Pressable, Text, View } from "react-native"; +import { StyleSheet, withUnistyles } from "react-native-unistyles"; +import type { Theme } from "@/styles/theme"; +import { HostPicker } from "./host-picker"; + +const ThemedChevronDown = withUnistyles(ChevronDown); +const mutedColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted }); + +interface HostSwitcherHost { + serverId: string; + label: string; +} + +/** A compact trigger showing the chosen host, opening the host picker to change it. */ +export function HostSwitcher({ + hosts, + value, + onSelect, + title, + accessibilityLabel, + testID, +}: { + hosts: HostSwitcherHost[]; + value: string; + onSelect: (serverId: string) => void; + /** The picker's title. */ + title: string; + /** Prefixes the selected host's label: "Plugin host: Laptop". */ + accessibilityLabel: string; + testID: string; +}) { + const [open, setOpen] = useState(false); + const anchorRef = useRef(null); + const selectedLabel = hosts.find((host) => host.serverId === value)?.label ?? value; + const openPicker = useCallback(() => setOpen(true), []); + return ( + + + + + {selectedLabel} + + + + + + ); +} + +const styles = StyleSheet.create((theme) => ({ + trigger: { + maxWidth: 180, + minHeight: 32, + paddingHorizontal: theme.spacing[2], + flexDirection: "row", + alignItems: "center", + gap: theme.spacing[1], + borderRadius: theme.borderRadius.md, + backgroundColor: theme.colors.surface1, + }, + label: { + flexShrink: 1, + color: theme.colors.foregroundMuted, + fontSize: theme.fontSize.base, + }, +})); diff --git a/packages/app/src/components/left-sidebar.tsx b/packages/app/src/components/left-sidebar.tsx index 4902a3ae93..b1de36c8ea 100644 --- a/packages/app/src/components/left-sidebar.tsx +++ b/packages/app/src/components/left-sidebar.tsx @@ -1,5 +1,5 @@ import { router } from "expo-router"; -import { FolderPlus, Gauge, GitBranch, Import, Server, Settings, X } from "lucide-react-native"; +import { FolderPlus, GitBranch, Import, Server, Settings, X } from "lucide-react-native"; import { useTranslation } from "react-i18next"; import { memo, useCallback, useEffect, useMemo, useRef, useState, type RefObject } from "react"; import { @@ -43,16 +43,15 @@ import type { SidebarWorkspaceGroup } from "@/components/sidebar/sidebar-labels" import type { SidebarProjectIconTarget } from "@/utils/sidebar-project-row-model"; import { type SidebarGroupMode, useSidebarViewStore } from "@/stores/sidebar-view-store"; import { useHosts } from "@/runtime/host-runtime"; +import { PluginSidebarItem } from "@/plugins/sidebar-items"; +import { useSidebarNavItems } from "@/sidebar-nav/use-sidebar-nav-items"; import { usePanelStore } from "@/stores/panel-store"; import { useOwnsWindowChromeCorner, WindowChromeSafeArea } from "@/utils/desktop-window"; import { useCloseAgentListGesture } from "@/mobile-panels/gestures"; import { MobilePanelOverlay } from "@/mobile-panels/presentation"; -import { - buildSettingsAddHostRoute, - buildSettingsRoute, - buildUsageRoute, -} from "@/utils/host-routes"; +import { buildSettingsAddHostRoute, buildSettingsRoute } from "@/utils/host-routes"; import { openHostOverview } from "@/navigation/settings-navigation"; +import { UsageSidebarItem } from "@/usage"; import { SidebarAgentListSkeleton } from "./sidebar-agent-list-skeleton"; import { SidebarCalloutSlot } from "./sidebar-callout-slot"; import { SidebarWorkspaceList } from "./sidebar-workspace-list"; @@ -430,25 +429,6 @@ function SidebarHostPicker({ ); } -function SidebarUsageButton({ theme }: { theme: SidebarTheme }) { - const { t } = useTranslation(); - const isCompactLayout = useIsCompactFormFactor(); - const showMobileAgent = usePanelStore((state) => state.showMobileAgent); - const handlePress = useCallback(() => { - if (isCompactLayout) showMobileAgent(); - router.push(buildUsageRoute()); - }, [isCompactLayout, showMobileAgent]); - return ( - - ); -} - function IconTooltipContent({ label, shortcutKeys, @@ -472,6 +452,7 @@ function SidebarFooter({ labels, handleAddHost, handleOpenHostSettings, + onBeforeNavigate, }: { theme: SidebarTheme; handleOpenProject: () => void; @@ -486,6 +467,7 @@ function SidebarFooter({ }; handleAddHost: () => void; handleOpenHostSettings: (serverId: string) => void; + onBeforeNavigate?: () => void; }) { const newAgentKeys = useShortcutKeys("new-agent"); const settingsKeys = useShortcutKeys("toggle-settings"); @@ -498,6 +480,7 @@ function SidebarFooter({ shortcutKeys={newAgentKeys} theme={theme} /> + - void }) { + const { items } = useSidebarNavItems("footer"); + const rowsRef = useRef(null); + const visibleItems = items.filter((item) => item.visible); + if (visibleItems.length === 0) return null; + return ( + + {visibleItems.map((item) => + item.kind === "plugin" ? ( + + ) : ( + + ), + )} + + ); +} + function MobileSidebar({ active, theme, @@ -634,6 +641,7 @@ function MobileSidebar({ labels={labels} handleAddHost={handleAddHost} handleOpenHostSettings={handleOpenHostSettings} + onBeforeNavigate={closeSidebar} /> @@ -954,29 +962,31 @@ const styles = StyleSheet.create((theme) => ({ fontWeight: theme.fontWeight.medium, }, sidebarFooter: { - flexDirection: "row", - alignItems: "center", - gap: theme.spacing[2], + gap: theme.spacing[1], paddingHorizontal: theme.spacing[2], paddingVertical: theme.spacing[3], borderTopWidth: 1, borderTopColor: theme.colors.border, }, + // Every footer row is 28px tall with `spacing[1]` between rows, the same as the gap above. + footerRows: { + gap: theme.spacing[1], + }, footerIconRow: { flexDirection: "row", alignItems: "center", gap: theme.spacing[2], - flexShrink: 0, }, + // The leading glyph sits on the footer icons' rail: a 16px icon centered in a 28px button + // starts 6px in, `spacing[1.5]`. footerAddProjectButton: { minWidth: 0, - minHeight: 32, - flex: 1, + minHeight: 28, flexDirection: "row", alignItems: "center", gap: theme.spacing[2], - paddingVertical: theme.spacing[1.5], - paddingHorizontal: theme.spacing[2], + paddingVertical: theme.spacing[1], + paddingHorizontal: theme.spacing[1.5], borderRadius: theme.borderRadius.lg, }, footerAddProjectButtonHovered: { diff --git a/packages/app/src/components/page-layout/index.tsx b/packages/app/src/components/page-layout/index.tsx index f210369184..e0c46b695a 100644 --- a/packages/app/src/components/page-layout/index.tsx +++ b/packages/app/src/components/page-layout/index.tsx @@ -12,6 +12,8 @@ interface PageLayoutProps { onBack?: () => void; testID?: string; titleTestID?: string; + /** Controls for the whole page: right of the title on desktop, in the back header on compact. */ + actions?: ReactNode; children: ReactNode; } @@ -20,7 +22,14 @@ interface PageLayoutProps { * document heading inside the page and the header only keeps the titlebar drag * region and window controls; on compact the title moves into a back header. */ -export function PageLayout({ title, onBack, testID, titleTestID, children }: PageLayoutProps) { +export function PageLayout({ + title, + onBack, + testID, + titleTestID, + actions, + children, +}: PageLayoutProps) { const isCompact = useIsCompactFormFactor(); const insets = useSafeAreaInsets(); const scrollContentStyle = useMemo(() => ({ paddingBottom: insets.bottom }), [insets.bottom]); @@ -28,14 +37,19 @@ export function PageLayout({ title, onBack, testID, titleTestID, children }: Pag return ( - {isCompact ? : } + {isCompact ? ( + + ) : ( + + )} {showTitle ? ( - + {title} + {actions} ) : null} {children} @@ -60,10 +74,16 @@ const styles = StyleSheet.create((theme) => ({ maxWidth: 720, alignSelf: "center", }, + titleRow: { + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + gap: theme.spacing[4], + marginBottom: theme.spacing[6], + }, title: { // Line up with the section titles, which sit inset from their cards. marginLeft: theme.spacing[1], - marginBottom: theme.spacing[6], fontSize: theme.fontSize["4xl"], fontWeight: theme.fontWeight.medium, color: theme.colors.foreground, diff --git a/packages/app/src/components/sidebar/sidebar-header-row.tsx b/packages/app/src/components/sidebar/sidebar-header-row.tsx index e30604ac9a..f59672d388 100644 --- a/packages/app/src/components/sidebar/sidebar-header-row.tsx +++ b/packages/app/src/components/sidebar/sidebar-header-row.tsx @@ -1,7 +1,13 @@ -import { useCallback, useMemo } from "react"; -import { Pressable, Text, View, type PressableStateCallbackType } from "react-native"; +import { + useCallback, + useMemo, + useState, + type ComponentType, + type ReactNode, + type Ref, +} from "react"; +import { Pressable, Text, View } from "react-native"; import { StyleSheet, withUnistyles } from "react-native-unistyles"; -import type { LucideIcon } from "lucide-react-native"; import { HEADER_INNER_HEIGHT, HEADER_INNER_HEIGHT_MOBILE } from "@/constants/layout"; import { ICON_SIZE } from "@/styles/theme"; import type { Theme } from "@/styles/theme"; @@ -11,10 +17,12 @@ import type { ShortcutKey } from "@/utils/format-shortcut"; const foregroundColorMapping = (theme: Theme) => ({ color: theme.colors.foreground }); const foregroundMutedColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted }); -type SidebarHeaderRowVariant = "header" | "compact"; +type SidebarHeaderRowVariant = "header" | "compact" | "inline"; + +export type SidebarRowIcon = ComponentType<{ size: number; color: string }>; interface SidebarHeaderRowProps { - icon: LucideIcon; + icon: SidebarRowIcon | null; label: string; onPress: () => void; isActive?: boolean; @@ -26,9 +34,18 @@ interface SidebarHeaderRowProps { * the lone header at the top of a sidebar (settings "Back to workspace"). * "compact": a row with no separator, for entries that * sit in a header group whose wrapper owns the single divider. + * "inline": a full-width row with no inset, for a row inside a padded container such as the + * sidebar footer. */ variant?: SidebarHeaderRowVariant; + /** Shown in the right slot while the row is hovered, when `trailing` is not set. */ shortcutKeys?: ShortcutKey[][] | null; + /** + * The right slot. A sibling of the row's button, never inside it (web cannot nest buttons). A + * press on the slot presses the row; a button inside it presses on its own. + */ + trailing?: ReactNode; + rowRef?: Ref; } export function SidebarHeaderRow({ @@ -41,71 +58,60 @@ export function SidebarHeaderRow({ accessibilityLabel, variant = "header", shortcutKeys = null, + trailing, + rowRef, }: SidebarHeaderRowProps) { - const ThemedIcon = useMemo(() => withUnistyles(Icon), [Icon]); + const [isHovered, setIsHovered] = useState(false); + const handlePointerEnter = useCallback(() => setIsHovered(true), []); + const handlePointerLeave = useCallback(() => setIsHovered(false), []); + const ThemedIcon = useMemo(() => (Icon ? withUnistyles(Icon) : null), [Icon]); + const isHighlighted = isHovered || isActive; + const iconSize = variant === "header" ? ICON_SIZE.md : ICON_SIZE.sm; - const containerStyle = useMemo( - () => (variant === "compact" ? styles.containerCompact : styles.container), - [variant], - ); - - const buttonStyle = useCallback( - ({ hovered }: PressableStateCallbackType & { hovered?: boolean }) => [ - styles.button, - (Boolean(hovered) || isActive) && styles.buttonHovered, - ], - [isActive], - ); - - const renderChildren = useCallback( - (state: PressableStateCallbackType & { hovered?: boolean }) => { - const isHighlighted = Boolean(state.hovered) || isActive; - return ( - <> - - - {shortcutKeys && Boolean(state.hovered) ? ( - - ) : null} - - ); - }, - [ThemedIcon, isActive, label, shortcutKeys, variant], - ); + let right = trailing ?? null; + if (right === null && shortcutKeys && isHovered) { + right = ; + } return ( - - + - {renderChildren} - + + {ThemedIcon ? ( + + ) : ( + + )} + {label} + + {right === null ? null : ( + + {right} + + )} + ); } -function SidebarHeaderRowLabel({ - label, - isHighlighted, -}: { - label: string; - isHighlighted: boolean; -}) { - const labelStyle = useMemo( - () => [styles.label, isHighlighted && styles.labelHighlighted], - [isHighlighted], - ); - return {label}; -} +const SELECTED_STATE = { selected: true } as const; const styles = StyleSheet.create((theme) => ({ container: { @@ -124,23 +130,49 @@ const styles = StyleSheet.create((theme) => ({ justifyContent: "center", userSelect: "none", }, + containerInline: { + width: "100%", + justifyContent: "center", + userSelect: "none", + }, + row: { + flexDirection: "row", + alignItems: "center", + // Same row geometry as the settings sidebar items. Shorter than the header + // strip so the hover highlight clears the strip's bottom separator. + minHeight: 28, + borderRadius: theme.borderRadius.lg, + }, + rowHighlighted: { + backgroundColor: theme.colors.surfaceSidebarHover, + }, button: { + flex: 1, + minWidth: 0, flexDirection: "row", alignItems: "center", gap: theme.spacing[2], - // Same row geometry as the settings sidebar items. Shorter than the header - // strip so the hover highlight clears the strip's bottom separator. minHeight: 28, paddingVertical: theme.spacing[1], // Match the project rows' inner padding so the icons align on one vertical // edge with the list below. paddingHorizontal: theme.spacing[2], - borderRadius: theme.borderRadius.lg, }, - buttonHovered: { - backgroundColor: theme.colors.surfaceSidebarHover, + // Footer rows put their icon on the footer's rail: where a 16px icon sits in a 28px button. + buttonInline: { + flex: 1, + minWidth: 0, + flexDirection: "row", + alignItems: "center", + gap: theme.spacing[2], + minHeight: 28, + paddingVertical: theme.spacing[1], + paddingHorizontal: theme.spacing[1.5], }, + iconSpacer: { width: ICON_SIZE.md, height: ICON_SIZE.md }, + iconSpacerCompact: { width: ICON_SIZE.sm, height: ICON_SIZE.sm }, label: { + flexShrink: 1, fontSize: theme.fontSize.base, fontWeight: theme.fontWeight.normal, color: theme.colors.foregroundMuted, @@ -148,7 +180,21 @@ const styles = StyleSheet.create((theme) => ({ labelHighlighted: { color: theme.colors.foreground, }, - shortcut: { - marginLeft: "auto", + trailing: { + flexDirection: "row", + alignItems: "center", + gap: theme.spacing[1], + paddingRight: theme.spacing[2], }, })); + +function getContainerStyle(variant: SidebarHeaderRowVariant) { + switch (variant) { + case "header": + return styles.container; + case "compact": + return styles.containerCompact; + case "inline": + return styles.containerInline; + } +} diff --git a/packages/app/src/components/sidebar/sidebar-nav-rows.tsx b/packages/app/src/components/sidebar/sidebar-nav-rows.tsx index 258dc19468..425280207e 100644 --- a/packages/app/src/components/sidebar/sidebar-nav-rows.tsx +++ b/packages/app/src/components/sidebar/sidebar-nav-rows.tsx @@ -1,11 +1,11 @@ import { router, usePathname } from "expo-router"; import { CalendarClock, History, Plus, Search } from "lucide-react-native"; -import { memo, useCallback, useMemo, type ComponentType } from "react"; +import { memo, useCallback, useMemo, useRef, type ComponentType } from "react"; import { useTranslation } from "react-i18next"; import { View, type StyleProp, type ViewStyle } from "react-native"; import { SidebarHeaderRow } from "@/components/sidebar/sidebar-header-row"; import { useShortcutKeys } from "@/hooks/use-shortcut-keys"; -import { PluginSidebarItemRow } from "@/plugins/sidebar-items"; +import { PluginSidebarItem } from "@/plugins/sidebar-items"; import { canCreateWorktreeForProjectKind } from "@/projects/host-projects"; import { useHostFeature } from "@/runtime/host-features"; import { @@ -38,19 +38,22 @@ interface SidebarNavRowsProps extends SidebarNavRowProps { * wrapper — when every item is hidden. */ export function SidebarNavRows({ style, onBeforeNavigate }: SidebarNavRowsProps) { - const { items } = useSidebarNavItems(); + const { items } = useSidebarNavItems("header"); const visibleItems = useMemo(() => items.filter((item) => item.visible), [items]); + const groupRef = useRef(null); if (visibleItems.length === 0) return null; return ( - + {visibleItems.map((item) => { if (item.kind === "plugin") { return ( - ); diff --git a/packages/app/src/components/sidebar/sidebar-popover.tsx b/packages/app/src/components/sidebar/sidebar-popover.tsx new file mode 100644 index 0000000000..ec13605d08 --- /dev/null +++ b/packages/app/src/components/sidebar/sidebar-popover.tsx @@ -0,0 +1,98 @@ +import { useCallback, type ReactNode, type RefObject } from "react"; +import type { View } from "react-native"; +import { MenuRoot, useMenuContext } from "@/components/ui/menu"; +import { PluginPopoverSurface } from "@/plugins/popover"; +import type { SidebarSection } from "@/sidebar-nav/model"; + +/** + * The popover a sidebar item opens: anchored to the item on wide layouts, a bottom sheet on + * compact ones. Plugin items and the built-in Usage item open it the same way: + * `SidebarPopoverRoot` around the item, `useSidebarPopoverAnchor` on the element to anchor to, + * and `SidebarPopoverSurface` for the body. + */ +export function SidebarPopoverRoot({ + open, + onOpenChange, + children, +}: { + open: boolean; + onOpenChange: (open: boolean) => void; + children: ReactNode; +}) { + return ( + + {children} + + ); +} + +function assignRef(ref: RefObject, node: View | null) { + Object.assign(ref, { current: node }); +} + +/** + * Where the popover anchors. `anchorTo` moves it to an element, such as the row just pressed; + * `offerAnchor` sets it only when nothing is anchored; `releaseAnchor` drops an element that is + * going away; `anchorToFallback` covers an item that anchored nothing. + */ +export function useSidebarPopoverAnchor(name: string): { + anchorTo: (node: View | null) => void; + offerAnchor: (node: View | null) => void; + releaseAnchor: (node: View | null) => void; + anchorToFallback: (fallback: RefObject) => void; +} { + const { triggerRef } = useMenuContext(name); + const anchorTo = useCallback((node: View | null) => assignRef(triggerRef, node), [triggerRef]); + const offerAnchor = useCallback( + (node: View | null) => { + if (!triggerRef.current) assignRef(triggerRef, node); + }, + [triggerRef], + ); + const releaseAnchor = useCallback( + (node: View | null) => { + if (triggerRef.current === node) assignRef(triggerRef, null); + }, + [triggerRef], + ); + const anchorToFallback = useCallback( + (fallback: RefObject) => offerAnchor(fallback.current), + [offerAnchor], + ); + return { anchorTo, offerAnchor, releaseAnchor, anchorToFallback }; +} + +const PLACEMENT = { + header: { side: "right", align: "start" }, + footer: { side: "top", align: "start" }, +} as const satisfies Record; + +export function SidebarPopoverSurface({ + section, + title, + sheetTrailing, + testID, + children, +}: { + section: SidebarSection; + /** The bottom sheet's title. */ + title: string; + /** Controls on the right of the bottom sheet's title. */ + sheetTrailing?: ReactNode; + testID: string; + children: ReactNode; +}) { + const placement = PLACEMENT[section]; + return ( + + {children} + + ); +} diff --git a/packages/app/src/components/ui/menu/menu-surface.tsx b/packages/app/src/components/ui/menu/menu-surface.tsx index 18cdb25dea..3a2729ad8e 100644 --- a/packages/app/src/components/ui/menu/menu-surface.tsx +++ b/packages/app/src/components/ui/menu/menu-surface.tsx @@ -105,6 +105,8 @@ export interface MenuSurfaceProps { pages?: readonly MenuPageDefinition[]; /** Title shown on the sheet's root page. Sheets always have a header; popovers never do. */ sheetTitle?: string; + /** Controls on the right of the sheet's root title. */ + sheetTrailing?: ReactNode; side?: Placement; align?: Alignment; offset?: number; @@ -339,6 +341,7 @@ function MenuSheetSurface({ children, pages = [], sheetTitle, + sheetTrailing, testID, keyboardFocusScope, }: MenuSurfaceProps): ReactElement | null { @@ -428,7 +431,9 @@ function MenuSheetSurface({ ) : ( <> - {sheetTitle ? : null} + {sheetTitle ? ( + + ) : null} {children} )} @@ -440,9 +445,11 @@ function MenuSheetSurface({ function MenuSheetHeader({ title, onBack, + trailing = null, }: { title: string; onBack: (() => void) | null; + trailing?: ReactNode; }): ReactElement { const { t } = useTranslation(); return ( @@ -462,6 +469,7 @@ function MenuSheetHeader({ {title} + {trailing ? {trailing} : null} ); } @@ -475,6 +483,11 @@ const styles = StyleSheet.create((theme) => ({ paddingTop: theme.spacing[1], paddingBottom: theme.spacing[3], }, + sheetTrailing: { + flex: 1, + flexDirection: "row", + justifyContent: "flex-end", + }, sheetBackButton: { width: 24, height: 24, diff --git a/packages/app/src/components/ui/status-badge.tsx b/packages/app/src/components/ui/status-badge.tsx index f2330e1b70..c1071ac882 100644 --- a/packages/app/src/components/ui/status-badge.tsx +++ b/packages/app/src/components/ui/status-badge.tsx @@ -8,17 +8,20 @@ interface StatusBadgeProps { label: string; variant?: StatusBadgeVariant; leading?: ReactNode; + /** `xs` fits beside a line of `sm` text: the same label on tighter padding. */ + size?: "sm" | "xs"; } -export function StatusBadge({ label, variant = "muted", leading }: StatusBadgeProps) { +export function StatusBadge({ label, variant = "muted", leading, size = "sm" }: StatusBadgeProps) { const pillStyle = useMemo( () => [ styles.pill, + size === "xs" && styles.pillXs, variant === "success" && styles.pillSuccess, variant === "warning" && styles.pillWarning, variant === "error" && styles.pillError, ], - [variant], + [size, variant], ); const textStyle = useMemo( () => [ @@ -50,6 +53,10 @@ const styles = StyleSheet.create((theme) => ({ paddingHorizontal: theme.spacing[2], paddingVertical: 3, }, + pillXs: { + paddingHorizontal: theme.spacing[1.5], + paddingVertical: 1, + }, pillSuccess: { backgroundColor: theme.colors.statusSuccessTint, borderColor: "transparent", diff --git a/packages/app/src/composer/index.tsx b/packages/app/src/composer/index.tsx index 28d23ae214..c46e2ac15a 100644 --- a/packages/app/src/composer/index.tsx +++ b/packages/app/src/composer/index.tsx @@ -48,7 +48,6 @@ import { type DraftAgentControlsProps, } from "@/composer/agent-controls"; import { ContextWindowMeter } from "@/components/context-window-meter"; -import { UsageComposerPill } from "@/usage"; import { useImageAttachmentPicker } from "@/hooks/use-image-attachment-picker"; import { selectAgentTurnPresentation, useSessionStore } from "@/stores/session-store"; import { useFilePicker } from "@/hooks/use-file-picker"; @@ -2093,13 +2092,8 @@ function ComposerContentImpl({ ], ); const beforeVoiceContent = useMemo( - () => ( - <> - {resolveContextWindowPlacement(contextWindowMeter, hasAgent)} - {hasAgent ? : null} - - ), - [agentId, contextWindowMeter, hasAgent, serverId], + () => <>{resolveContextWindowPlacement(contextWindowMeter, hasAgent)}, + [contextWindowMeter, hasAgent], ); const hasGithubAttachment = useMemo( diff --git a/packages/app/src/hooks/use-settings/storage.test.ts b/packages/app/src/hooks/use-settings/storage.test.ts index df738d7247..3797d76e8b 100644 --- a/packages/app/src/hooks/use-settings/storage.test.ts +++ b/packages/app/src/hooks/use-settings/storage.test.ts @@ -238,6 +238,75 @@ describe("loadAppSettingsFromStorage", () => { expect(result.sidebarNavItems).toEqual([]); }); + it("loads stored sidebar footer items in order and defaults them to empty", async () => { + expect((await loadAppSettingsFromStorage(makeDeps())).sidebarFooterItems).toEqual([]); + + const deps = makeDeps({ + storage: createInMemoryKeyValueStorage({ + [APP_SETTINGS_KEY]: JSON.stringify({ + sidebarFooterItems: [ + // A footer button that was configurable once; the sidebar model skips it. + { key: "help", visible: false }, + { key: "plugin:sync:status", visible: true }, + { key: "usage", visible: false }, + ], + }), + }), + }); + + const result = await loadAppSettingsFromStorage(deps); + + expect(result.sidebarFooterItems).toEqual([ + { key: "help", visible: false }, + { key: "plugin:sync:status", visible: true }, + { key: "usage", visible: false }, + ]); + }); + + it("loads usage preferences with pins in order and defaults a fresh device to none", async () => { + expect((await loadAppSettingsFromStorage(makeDeps())).usage).toEqual({ + displayAs: "used", + pinned: [], + }); + + const deps = makeDeps({ + storage: createInMemoryKeyValueStorage({ + [APP_SETTINGS_KEY]: JSON.stringify({ + usage: { + displayAs: "remaining", + pinned: [ + { sourceId: "codex", windowId: "weekly" }, + { sourceId: "claude", windowId: "five-hour" }, + ], + }, + }), + }), + }); + + expect((await loadAppSettingsFromStorage(deps)).usage).toEqual({ + displayAs: "remaining", + pinned: [ + { sourceId: "codex", windowId: "weekly" }, + { sourceId: "claude", windowId: "five-hour" }, + ], + }); + }); + + it("keeps valid usage preferences when one field is malformed", async () => { + const deps = makeDeps({ + storage: createInMemoryKeyValueStorage({ + [APP_SETTINGS_KEY]: JSON.stringify({ + usage: { displayAs: "percent", pinned: [{ sourceId: "claude", windowId: "weekly" }] }, + }), + }), + }); + + expect((await loadAppSettingsFromStorage(deps)).usage).toEqual({ + displayAs: "used", + pinned: [{ sourceId: "claude", windowId: "weekly" }], + }); + }); + it("collapses legacy diff destinations into the former Explorer choice", async () => { const deps = makeDeps({ storage: createInMemoryKeyValueStorage({ diff --git a/packages/app/src/hooks/use-settings/storage.ts b/packages/app/src/hooks/use-settings/storage.ts index 7171505492..a954074e8b 100644 --- a/packages/app/src/hooks/use-settings/storage.ts +++ b/packages/app/src/hooks/use-settings/storage.ts @@ -4,6 +4,11 @@ import type { QueryClient } from "@tanstack/react-query"; import type { DesktopSettings } from "@/desktop/settings/desktop-settings"; import type { AppLanguage } from "@/i18n/locales"; import type { SidebarNavPreference } from "@/sidebar-nav/model"; +import { + DEFAULT_USAGE_PREFERENCES, + UsagePreferencesSchema, + type UsagePreferences, +} from "@/usage/preferences"; import { DEFAULT_SIDEBAR_CHECKS_DISPLAY, type SidebarChecksDisplay, @@ -91,6 +96,10 @@ export interface AppSettings { sidebarChecksDisplay: SidebarChecksDisplay; /** Top-level sidebar rows in display order; empty means the default order, all visible. */ sidebarNavItems: SidebarNavPreference[]; + /** Sidebar footer items in display order; empty means the default order, all visible. */ + sidebarFooterItems: SidebarNavPreference[]; + /** How usage reads and which windows the sidebar summary shows. */ + usage: UsagePreferences; autoExpandReasoning: boolean; toolCallDetailLevel: ToolCallDetailLevel; chatOutlineEnabled: boolean; @@ -145,6 +154,8 @@ export const DEFAULT_CLIENT_SETTINGS: AppSettings = { sidebarRowItems: DEFAULT_SIDEBAR_ROW_ITEMS, sidebarChecksDisplay: DEFAULT_SIDEBAR_CHECKS_DISPLAY, sidebarNavItems: [], + sidebarFooterItems: [], + usage: DEFAULT_USAGE_PREFERENCES, autoExpandReasoning: false, toolCallDetailLevel: "detailed", chatOutlineEnabled: true, @@ -238,6 +249,8 @@ const StoredAppSettingsSchema = z .optional() .catch(DEFAULT_SIDEBAR_CHECKS_DISPLAY), sidebarNavItems: z.array(z.object({ key: z.string(), visible: z.boolean() })).catch([]), + sidebarFooterItems: z.array(z.object({ key: z.string(), visible: z.boolean() })).catch([]), + usage: UsagePreferencesSchema, autoExpandReasoning: z.boolean().catch(false), toolCallDetailLevel: z .enum(["overview", "detailed"]) diff --git a/packages/app/src/i18n/resources/ar.ts b/packages/app/src/i18n/resources/ar.ts index c2a1475776..658c1981df 100644 --- a/packages/app/src/i18n/resources/ar.ts +++ b/packages/app/src/i18n/resources/ar.ts @@ -1147,6 +1147,9 @@ export const ar: TranslationResources = { settings: "إعدادات", closeSidebar: "إغلاق الشريط الجانبي", }, + footer: { + usage: "الاستخدام", + }, help: { trigger: "المساعدة والدعم", sectionHelp: "المساعدة", @@ -2168,8 +2171,15 @@ export const ar: TranslationResources = { description: "عرض مخطط للتنقل بين المطالبات", }, sidebar: { - title: "الشريط الجانبي", - description: "اختر العناصر التي تظهر أعلى الشريط الجانبي وترتيبها", + header: { + title: "الرأس", + description: "اختر العناصر التي تظهر أعلى الشريط الجانبي وترتيبها", + }, + footer: { + title: "التذييل", + description: + "اختر الصفوف التي تظهر أسفل الشريط الجانبي وترتيبها. يظهر «إضافة مشروع» وصف الأيقونات دائمًا", + }, moveUp: "نقل لأعلى", moveDown: "نقل لأسفل", }, diff --git a/packages/app/src/i18n/resources/en.ts b/packages/app/src/i18n/resources/en.ts index 28600d410c..80a137ccf3 100644 --- a/packages/app/src/i18n/resources/en.ts +++ b/packages/app/src/i18n/resources/en.ts @@ -1155,6 +1155,9 @@ export const en = { settings: "Settings", closeSidebar: "Close sidebar", }, + footer: { + usage: "Usage", + }, help: { trigger: "Help and support", sectionHelp: "Help", @@ -2292,8 +2295,15 @@ export const en = { description: "Show an outline for jumping between prompts", }, sidebar: { - title: "Sidebar", - description: "Choose which items appear at the top of the sidebar and in what order", + header: { + title: "Header", + description: "Choose which items appear at the top of the sidebar and in what order", + }, + footer: { + title: "Footer", + description: + "Choose which rows appear at the bottom of the sidebar and in what order. Add project and the icon row always show", + }, moveUp: "Move up", moveDown: "Move down", }, diff --git a/packages/app/src/i18n/resources/es.ts b/packages/app/src/i18n/resources/es.ts index 7b9cf848df..74ac40906b 100644 --- a/packages/app/src/i18n/resources/es.ts +++ b/packages/app/src/i18n/resources/es.ts @@ -1183,6 +1183,9 @@ export const es: TranslationResources = { settings: "Ajustes", closeSidebar: "Cerrar barra lateral", }, + footer: { + usage: "Uso", + }, help: { trigger: "Ayuda y soporte", sectionHelp: "Ayuda", @@ -2220,9 +2223,16 @@ export const es: TranslationResources = { description: "Muestra un esquema para saltar entre instrucciones", }, sidebar: { - title: "Barra lateral", - description: - "Elige qué elementos aparecen en la parte superior de la barra lateral y en qué orden", + header: { + title: "Encabezado", + description: + "Elige qué elementos aparecen en la parte superior de la barra lateral y en qué orden", + }, + footer: { + title: "Pie", + description: + "Elige qué filas aparecen en la parte inferior de la barra lateral y en qué orden. Añadir proyecto y la fila de iconos siempre se muestran", + }, moveUp: "Mover hacia arriba", moveDown: "Mover hacia abajo", }, diff --git a/packages/app/src/i18n/resources/fr.ts b/packages/app/src/i18n/resources/fr.ts index 15b8cc9944..d5b811f49c 100644 --- a/packages/app/src/i18n/resources/fr.ts +++ b/packages/app/src/i18n/resources/fr.ts @@ -1183,6 +1183,9 @@ export const fr: TranslationResources = { settings: "Paramètres", closeSidebar: "Fermer la barre latérale", }, + footer: { + usage: "Utilisation", + }, help: { trigger: "Aide et assistance", sectionHelp: "Aide", @@ -2224,8 +2227,16 @@ export const fr: TranslationResources = { description: "Afficher un plan pour passer d’une requête à l’autre", }, sidebar: { - title: "Barre latérale", - description: "Choisissez les éléments affichés en haut de la barre latérale et leur ordre", + header: { + title: "En-tête", + description: + "Choisissez les éléments affichés en haut de la barre latérale et leur ordre", + }, + footer: { + title: "Pied", + description: + "Choisissez les lignes affichées en bas de la barre latérale et leur ordre. Ajouter un projet et la rangée d’icônes restent toujours visibles", + }, moveUp: "Déplacer vers le haut", moveDown: "Déplacer vers le bas", }, diff --git a/packages/app/src/i18n/resources/ja.ts b/packages/app/src/i18n/resources/ja.ts index 6ccbb538b5..b674fec325 100644 --- a/packages/app/src/i18n/resources/ja.ts +++ b/packages/app/src/i18n/resources/ja.ts @@ -1161,6 +1161,9 @@ export const ja: TranslationResources = { settings: "設定", closeSidebar: "サイドバーを閉じる", }, + footer: { + usage: "使用状況", + }, help: { trigger: "ヘルプとサポート", sectionHelp: "ヘルプ", @@ -2187,8 +2190,15 @@ export const ja: TranslationResources = { description: "プロンプト間を移動するためのアウトラインを表示します", }, sidebar: { - title: "サイドバー", - description: "サイドバー上部に表示する項目とその順序を選択します", + header: { + title: "ヘッダー", + description: "サイドバー上部に表示する項目とその順序を選択します", + }, + footer: { + title: "フッター", + description: + "サイドバー下部に表示する行とその順序を選択します。プロジェクトを追加とアイコンの行は常に表示されます", + }, moveUp: "上に移動", moveDown: "下に移動", }, diff --git a/packages/app/src/i18n/resources/ko.ts b/packages/app/src/i18n/resources/ko.ts index 76dfb606ad..5f2aa560fd 100644 --- a/packages/app/src/i18n/resources/ko.ts +++ b/packages/app/src/i18n/resources/ko.ts @@ -1154,6 +1154,9 @@ export const ko: TranslationResources = { settings: "설정", closeSidebar: "사이드바 닫기", }, + footer: { + usage: "사용량", + }, help: { trigger: "도움말 및 지원", sectionHelp: "도움말", @@ -2179,8 +2182,15 @@ export const ko: TranslationResources = { description: "프롬프트 사이를 이동하기 위한 개요 표시", }, sidebar: { - title: "사이드바", - description: "사이드바 상단에 표시할 항목과 순서를 선택하세요", + header: { + title: "헤더", + description: "사이드바 상단에 표시할 항목과 순서를 선택하세요", + }, + footer: { + title: "푸터", + description: + "사이드바 하단에 표시할 행과 순서를 선택하세요. 프로젝트 추가와 아이콘 행은 항상 표시됩니다", + }, moveUp: "위로 이동", moveDown: "아래로 이동", }, diff --git a/packages/app/src/i18n/resources/pt-BR.ts b/packages/app/src/i18n/resources/pt-BR.ts index 0c747106c6..bf3704bf63 100644 --- a/packages/app/src/i18n/resources/pt-BR.ts +++ b/packages/app/src/i18n/resources/pt-BR.ts @@ -1173,6 +1173,9 @@ export const ptBR: TranslationResources = { settings: "Configurações", closeSidebar: "Fechar barra lateral", }, + footer: { + usage: "Uso", + }, help: { trigger: "Ajuda e suporte", sectionHelp: "Ajuda", @@ -2203,8 +2206,15 @@ export const ptBR: TranslationResources = { description: "Mostrar uma estrutura para navegar entre prompts", }, sidebar: { - title: "Barra lateral", - description: "Escolha quais itens aparecem no topo da barra lateral e em que ordem", + header: { + title: "Cabeçalho", + description: "Escolha quais itens aparecem no topo da barra lateral e em que ordem", + }, + footer: { + title: "Rodapé", + description: + "Escolha quais linhas aparecem na parte inferior da barra lateral e em que ordem. Adicionar projeto e a linha de ícones sempre aparecem", + }, moveUp: "Mover para cima", moveDown: "Mover para baixo", }, diff --git a/packages/app/src/i18n/resources/ru.ts b/packages/app/src/i18n/resources/ru.ts index 7e3fe4f436..ed8521e540 100644 --- a/packages/app/src/i18n/resources/ru.ts +++ b/packages/app/src/i18n/resources/ru.ts @@ -1165,6 +1165,9 @@ export const ru: TranslationResources = { settings: "Настройки", closeSidebar: "Закрыть боковую панель", }, + footer: { + usage: "Использование", + }, help: { trigger: "Помощь и поддержка", sectionHelp: "Помощь", @@ -2206,9 +2209,16 @@ export const ru: TranslationResources = { description: "Показывать оглавление для перехода между запросами", }, sidebar: { - title: "Боковая панель", - description: - "Выберите, какие элементы отображаются вверху боковой панели и в каком порядке", + header: { + title: "Верх", + description: + "Выберите, какие элементы отображаются вверху боковой панели и в каком порядке", + }, + footer: { + title: "Низ", + description: + "Выберите, какие строки отображаются внизу боковой панели и в каком порядке. «Добавить проект» и ряд значков видны всегда", + }, moveUp: "Переместить вверх", moveDown: "Переместить вниз", }, diff --git a/packages/app/src/i18n/resources/zh-CN.ts b/packages/app/src/i18n/resources/zh-CN.ts index c54dbae802..d01d7f5130 100644 --- a/packages/app/src/i18n/resources/zh-CN.ts +++ b/packages/app/src/i18n/resources/zh-CN.ts @@ -1139,6 +1139,9 @@ export const zhCN: TranslationResources = { settings: "设置", closeSidebar: "关闭侧边栏", }, + footer: { + usage: "使用情况", + }, help: { trigger: "帮助与支持", sectionHelp: "帮助", @@ -2144,8 +2147,14 @@ export const zhCN: TranslationResources = { description: "显示用于在提示词之间跳转的大纲", }, sidebar: { - title: "侧边栏", - description: "选择侧边栏顶部显示的项目及其顺序", + header: { + title: "顶部", + description: "选择侧边栏顶部显示的项目及其顺序", + }, + footer: { + title: "底部", + description: "选择侧边栏底部显示的行及其顺序。添加项目和图标行始终显示", + }, moveUp: "上移", moveDown: "下移", }, diff --git a/packages/app/src/plugins/actions.ts b/packages/app/src/plugins/actions.ts index 9f2329df09..b61eae7c1d 100644 --- a/packages/app/src/plugins/actions.ts +++ b/packages/app/src/plugins/actions.ts @@ -3,16 +3,17 @@ import type { PluginAgentCommandContext, PluginCommandCapabilities, PluginPanelLocation, + PluginScreenParams, PluginWorkspaceCommandContext, } from "@getpaseo/plugin/client"; import type { PluginClientStateSource } from "@getpaseo/plugin/client/host"; import { resolvePluginPanelOpenLocation } from "./workspace-panels/locations"; -import type { PluginSurfaceRuntime } from "./surface-runtime"; +import { parsePluginOpenScreenInput } from "./surface-contribution"; import type { InstalledPlugin } from "./types"; export interface PluginNavigation { openSettings(pluginId: string, screenId: string): void; - openSurface(pluginId: string, surfaceId: string): void; + openSurface(pluginId: string, surfaceId: string, params?: PluginScreenParams): void; openWorkspacePanel(pluginId: string, panelId: string, location: PluginPanelLocation): void; openAgentPanel( pluginId: string, @@ -24,41 +25,40 @@ export interface PluginNavigation { export function createPluginCapabilities( plugin: InstalledPlugin, - runtime: PluginSurfaceRuntime, navigation: PluginNavigation, ): PluginCommandCapabilities { + function openScreen(input: unknown) { + const { screenId, params } = parsePluginOpenScreenInput(plugin, input); + navigation.openSurface(plugin.id, screenId, params); + } return { - paseo: runtime.paseo, - rpc: (contract, input) => callPluginRpc(contract, runtime.invoke, input), + paseo: plugin.paseo, + rpc: (contract, input) => callPluginRpc(contract, plugin.invoke, input), openSettings(screenId) { if (!plugin.settingsScreens.some((screen) => screen.id === screenId)) throw new Error(`Plugin settings screen is unavailable: ${screenId}`); navigation.openSettings(plugin.id, screenId); }, - openSurface(surfaceId) { - if (!plugin.surfaces.some((surface) => surface.id === surfaceId)) { - throw new Error(`Plugin surface is unavailable: ${surfaceId}`); - } - navigation.openSurface(plugin.id, surfaceId); - }, + openScreen, + // COMPAT(pluginSidebarAliases): added in v0.11.0, remove after 2027-03-29 + openSurface: (screenId) => openScreen({ screenId }), }; } export function createPluginAgentActionContext(input: { plugin: InstalledPlugin; - runtime: PluginSurfaceRuntime; navigation: PluginNavigation; state: PluginClientStateSource; workspaceId: string; agentId: string; }): PluginAgentCommandContext | null { - const { plugin, runtime, navigation, state, workspaceId, agentId } = input; + const { plugin, navigation, state, workspaceId, agentId } = input; const workspace = state.getWorkspace(workspaceId); const agent = state.getAgent(agentId); if (!workspace || !agent || agent.workspaceId !== workspace.id) return null; return { context: "agent", - ...createPluginCapabilities(plugin, runtime, navigation), + ...createPluginCapabilities(plugin, navigation), workspace, agent, openPanel(panelId, options) { @@ -76,17 +76,16 @@ export function createPluginAgentActionContext(input: { export function createPluginWorkspaceActionContext(input: { plugin: InstalledPlugin; - runtime: PluginSurfaceRuntime; navigation: PluginNavigation; state: PluginClientStateSource; workspaceId: string; }): PluginWorkspaceCommandContext | null { - const { plugin, runtime, navigation, state, workspaceId } = input; + const { plugin, navigation, state, workspaceId } = input; const workspace = state.getWorkspace(workspaceId); if (!workspace) return null; return { context: "workspace", - ...createPluginCapabilities(plugin, runtime, navigation), + ...createPluginCapabilities(plugin, navigation), workspace, openPanel(panelId, options) { const panel = plugin.workspacePanels.find( diff --git a/packages/app/src/plugins/buttons/model.test.ts b/packages/app/src/plugins/buttons/model.test.ts index 028114c862..4a77a81311 100644 --- a/packages/app/src/plugins/buttons/model.test.ts +++ b/packages/app/src/plugins/buttons/model.test.ts @@ -11,10 +11,13 @@ function installation(): InstalledPlugin { serverId: "host-a", clientBundle: "bundle", queryClient: new QueryClient(), + paseo: {} as InstalledPlugin["paseo"], + invoke: async () => undefined, cleanup: () => undefined, settingsScreens: [], surfaces: [], - sidebarItems: [], + sidebarItems: { header: [], footer: [] }, + legacySidebarItems: [], workspacePanels: [], commandCenterItems: [], clientSlashCommands: [], diff --git a/packages/app/src/plugins/buttons/view.tsx b/packages/app/src/plugins/buttons/view.tsx index b737b3ebf0..7eae9a046d 100644 --- a/packages/app/src/plugins/buttons/view.tsx +++ b/packages/app/src/plugins/buttons/view.tsx @@ -1,4 +1,3 @@ -import { PluginClientStateProvider } from "@getpaseo/plugin/client/host"; import type { PluginButtonBehavior, PluginButtonIcon, @@ -7,14 +6,13 @@ import type { } from "@getpaseo/plugin/client"; import type { PluginTheme } from "@getpaseo/plugin"; import { useCallback, useMemo, useSyncExternalStore, type ReactNode } from "react"; -import { Platform, Pressable, Text, View, useWindowDimensions } from "react-native"; +import { Pressable, Text, View, useWindowDimensions } from "react-native"; import { StyleSheet, withUnistyles } from "react-native-unistyles"; import { AlertCircle, ChevronDown, MoreHorizontal } from "lucide-react-native"; import { useTranslation } from "react-i18next"; import { MenuRoot, MenuTrigger, - MenuSurface, MenuItem, MenuSeparator, MenuSubTrigger, @@ -30,22 +28,27 @@ import { } from "@/components/ui/icon-button-chrome"; import { composerPillStyles } from "@/composer/pill-styles"; import { useIsCompactFormFactor } from "@/constants/layout"; -import { ToastApiProvider, useToast } from "@/contexts/toast-context"; +import { useToast } from "@/contexts/toast-context"; import { useHostRuntimeClient, useHosts } from "@/runtime/host-runtime"; import type { Theme } from "@/styles/theme"; import { createPluginClientStateSource } from "../client-state/source"; import { Icon } from "../icons"; -import { PluginRuntimeBoundary } from "../runtime-boundary"; +import { + PluginEnvironmentProvider, + PluginPopoverContent, + PluginPopoverSurface, + type PluginEnvironment, +} from "../popover"; import { SurfaceErrorBoundary } from "../surface-error-boundary"; import { toPluginTheme } from "../theme"; import { buttonMatches, type RegisteredPluginButton } from "./model"; import { pluginButtonStore } from "./store"; +import { resolvePluginPlatform } from "../platform"; interface ButtonView { entry: RegisteredPluginButton; props: PluginHostProps & RegisteredPluginButton["context"]; - client: NonNullable>; - state: ReturnType; + environment: PluginEnvironment; toast: ReturnType; } @@ -67,21 +70,9 @@ function headerButtonStyle(compact: boolean, state: IconButtonChromeState, disab ]; } -function resolvePlatform(): PluginHostProps["layout"]["platform"] { - if (Platform.OS === "ios") return "ios"; - if (Platform.OS === "android") return "android"; - return "web"; -} - -// These providers live inside the surface content as well as around its trigger. Native sheets -// teleport their children, so providers around MenuRoot alone cannot reach the plugin body. function ButtonEnvironment({ view, children }: { view: ButtonView; children: ReactNode }) { return ( - - - {children} - - + {children} ); } @@ -184,9 +175,9 @@ function ButtonBody({ if (behavior.kind === "popover") { const Content = behavior.Content; return ( - + - + ); } if (behavior.kind !== "menu") return null; @@ -336,19 +327,15 @@ function ButtonControl({ view }: { view: ButtonView }) { {expanded ? ( - - + ) : null} ); @@ -398,14 +385,17 @@ function createButtonView({ if (!client) return null; return { entry, - client, toast, - state: createPluginClientStateSource(entry.installation.serverId), + environment: { + installation: entry.installation, + toast, + state: createPluginClientStateSource(entry.installation.serverId), + }, props: { ...entry.context, theme, host: { id: entry.installation.serverId, label: hostLabel }, - layout: { compact, platform: resolvePlatform() }, + layout: { compact, platform: resolvePluginPlatform() }, }, }; } @@ -511,17 +501,13 @@ function OverflowPages({ }, [entries, client, toast, theme, hostLabel, compact]); const { t } = useTranslation(); return ( - {menuContent.rows} - + ); } @@ -665,5 +651,4 @@ const styles = StyleSheet.create((theme) => ({ flexShrink: 0, overflow: "hidden", }, - content: { padding: theme.spacing[3], gap: theme.spacing[2] }, })); diff --git a/packages/app/src/plugins/client-runtime.ts b/packages/app/src/plugins/client-runtime.ts index 6e7b13501b..68534a0579 100644 --- a/packages/app/src/plugins/client-runtime.ts +++ b/packages/app/src/plugins/client-runtime.ts @@ -1,6 +1,5 @@ import { getHostRuntimeStore } from "@/runtime/host-runtime"; import { createPluginHosts } from "./hosts"; -import type { DaemonClient } from "@getpaseo/client/internal/daemon-client"; import type { PluginClientOpenPanelOptions } from "@getpaseo/plugin/client"; import { createPluginAgentActionContext, @@ -11,19 +10,12 @@ import { createPluginClientStateSource } from "./client-state/source"; import type { PluginClientRuntime } from "./evaluate"; import { createPluginNavigation } from "./navigation"; import { pluginButtonStore } from "./buttons"; -import { createPluginSurfaceRuntime } from "./surface-runtime"; import type { InstalledPlugin } from "./types"; -export function createPluginClientRuntime( - installation: InstalledPlugin, - daemonClient: DaemonClient, -): PluginClientRuntime { - const runtime = createPluginSurfaceRuntime(daemonClient, installation); - if (!runtime) throw new Error("Plugin host is offline"); +export function createPluginClientRuntime(installation: InstalledPlugin): PluginClientRuntime { const state = createPluginClientStateSource(installation.serverId); const capabilities = createPluginCapabilities( installation, - runtime, createPluginNavigation({ serverId: installation.serverId, workspaceId: null }), ); return { @@ -36,26 +28,24 @@ export function createPluginClientRuntime( return pluginButtonStore.addHeaderButton(installation, contribution); }, openPanel(panelId, options) { - openClientPanel({ installation, runtime, state, panelId, options }); + openClientPanel({ installation, state, panelId, options }); }, }; } function openClientPanel(input: { installation: InstalledPlugin; - runtime: NonNullable>; state: ReturnType; panelId: string; options: PluginClientOpenPanelOptions; }): void { - const { installation, runtime, state, panelId, options } = input; + const { installation, state, panelId, options } = input; const workspaceId = options.workspaceId.trim(); const agentId = options.agentId?.trim(); const navigation = createPluginNavigation({ serverId: installation.serverId, workspaceId }); const action = agentId ? createPluginAgentActionContext({ plugin: installation, - runtime, navigation, state, workspaceId, @@ -63,7 +53,6 @@ function openClientPanel(input: { }) : createPluginWorkspaceActionContext({ plugin: installation, - runtime, navigation, state, workspaceId, diff --git a/packages/app/src/plugins/client-slash-commands/index.ts b/packages/app/src/plugins/client-slash-commands/index.ts index 57d205a7b9..9ab66ac195 100644 --- a/packages/app/src/plugins/client-slash-commands/index.ts +++ b/packages/app/src/plugins/client-slash-commands/index.ts @@ -1,10 +1,8 @@ import { useMemo } from "react"; -import { useHostRuntimeClient } from "@/runtime/host-runtime"; import { createPluginAgentActionContext, createPluginWorkspaceActionContext } from "../actions"; import { createPluginClientStateSource } from "../client-state/source"; import { createPluginNavigation } from "../navigation"; import { useInstalledPlugins } from "../registry"; -import { createPluginSurfaceRuntime } from "../surface-runtime"; export interface PluginClientSlashCommand { pluginId: string; @@ -19,10 +17,9 @@ export function usePluginClientSlashCommands(input: { workspaceId: string | null | undefined; agentId: string; }): PluginClientSlashCommand[] { - const client = useHostRuntimeClient(input.serverId); const installed = useInstalledPlugins(); return useMemo(() => { - if (!client || !input.workspaceId) return []; + if (!input.workspaceId) return []; const workspaceId = input.workspaceId; const state = createPluginClientStateSource(input.serverId); const navigation = createPluginNavigation({ @@ -42,38 +39,30 @@ export function usePluginClientSlashCommands(input: { description: contribution.description, argumentHint: contribution.argumentHint, async run(args: string) { - const runtime = createPluginSurfaceRuntime(client, plugin); - if (!runtime) return; - try { - const context = - contribution.context === "agent" - ? createPluginAgentActionContext({ - plugin, - runtime, - navigation, - state, - workspaceId, - agentId: input.agentId, - }) - : createPluginWorkspaceActionContext({ - plugin, - runtime, - navigation, - state, - workspaceId, - }); - if (contribution.context === "agent" && context?.context === "agent") - await contribution.onSubmit({ ...context, args }); - else if (contribution.context === "workspace" && context?.context === "workspace") - await contribution.onSubmit({ ...context, args }); - } finally { - await runtime.paseo.dispose(); - } + const context = + contribution.context === "agent" + ? createPluginAgentActionContext({ + plugin, + navigation, + state, + workspaceId, + agentId: input.agentId, + }) + : createPluginWorkspaceActionContext({ + plugin, + navigation, + state, + workspaceId, + }); + if (contribution.context === "agent" && context?.context === "agent") + await contribution.onSubmit({ ...context, args }); + else if (contribution.context === "workspace" && context?.context === "workspace") + await contribution.onSubmit({ ...context, args }); }, }, ]; }), ); return commands; - }, [client, input.agentId, input.serverId, input.workspaceId, installed]); + }, [input.agentId, input.serverId, input.workspaceId, installed]); } diff --git a/packages/app/src/plugins/command-center/contributions.test.ts b/packages/app/src/plugins/command-center/contributions.test.ts index 89313f907c..a2544bd034 100644 --- a/packages/app/src/plugins/command-center/contributions.test.ts +++ b/packages/app/src/plugins/command-center/contributions.test.ts @@ -61,11 +61,23 @@ function plugin(onAgentSelect: AgentCommandItem["onSelect"]): InstalledPlugin { serverId: "host-1", clientBundle: "bundle", lifetime: new AbortController(), + paseo: createPaseoApi( + new DaemonClient({ + url: "ws://127.0.0.1:1", + clientId: "plugin-command-test", + clientType: "cli", + }), + ), + invoke: async (method: string, input: unknown) => { + expect(method).toBe("review.inspect"); + return { value: inspect.input.parse(input).value + 1 }; + }, queryClient: new QueryClient(), cleanup: () => {}, settingsScreens: [], - surfaces: [{ id: "main", Component: () => null }], - sidebarItems: [], + surfaces: [{ id: "main", title: "Main", Component: () => null }], + sidebarItems: { header: [], footer: [] }, + legacySidebarItems: [], workspacePanels: [ { id: "details", @@ -121,28 +133,11 @@ function stateSource() { }; } -function createRuntime(installed: InstalledPlugin) { - const client = new DaemonClient({ - url: "ws://127.0.0.1:1", - clientId: "plugin-command-test", - clientType: "cli", - }); - return { - paseo: createPaseoApi(client), - invoke: async (method: string, input: unknown) => { - expect(installed.id).toBe("review"); - expect(method).toBe("review.inspect"); - return { value: inspect.input.parse(input).value + 1 }; - }, - }; -} - describe("plugin Command Center contributions", () => { it("shows only contributions whose synchronous context exists", () => { const installed = plugin(() => undefined); const common = { plugins: [installed], - runtime: createRuntime, state: stateSource(), navigation: { openSettings() {}, @@ -188,10 +183,8 @@ describe("plugin Command Center contributions", () => { context.openSurface("main"); context.openPanel("details", { location: "explorer" }); }); - const runtime = createRuntime(installed); const actions = buildPluginCommandCenterContributions({ plugins: [installed], - runtime: () => runtime, state: stateSource(), workspaceId: workspace.id, agentId: agent.id, @@ -215,7 +208,8 @@ describe("plugin Command Center contributions", () => { await actions.find((action) => action.id === "review:agent")?.run(); expect(rpcValue).toBe(5); - expect(receivedPaseo).toBe(runtime.paseo); + // Commands use the plugin's one client. + expect(receivedPaseo).toBe(installed.paseo); expect(opened).toEqual(["review/surface/main", "review/agent/details/agent-1/explorer"]); }); @@ -223,7 +217,6 @@ describe("plugin Command Center contributions", () => { expect( buildPluginCommandCenterContributions({ plugins: [], - runtime: createRuntime, state: stateSource(), workspaceId: workspace.id, agentId: agent.id, diff --git a/packages/app/src/plugins/command-center/contributions.ts b/packages/app/src/plugins/command-center/contributions.ts index 9d40ee57e2..be4cb8068a 100644 --- a/packages/app/src/plugins/command-center/contributions.ts +++ b/packages/app/src/plugins/command-center/contributions.ts @@ -3,13 +3,11 @@ import type { CommandCenterContribution } from "@/command-center/contributions"; import { getCommandCenterIcon } from "@/command-center/icon"; import { resolvePluginIcon } from "../icons"; import { resolvePluginPanelOpenLocation } from "../workspace-panels/locations"; -import type { PluginSurfaceRuntime } from "../surface-runtime"; import type { InstalledPlugin } from "../types"; import { createPluginCapabilities, type PluginNavigation } from "../actions"; export interface PluginCommandCenterSource { plugins: readonly InstalledPlugin[]; - runtime(plugin: InstalledPlugin): PluginSurfaceRuntime; state: PluginClientStateSource; workspaceId: string | null; agentId: string | null; @@ -26,8 +24,7 @@ export function buildPluginCommandCenterContributions( if (item.context === "workspace" && !source.workspaceId) continue; if (item.context === "agent" && (!source.workspaceId || !source.agentId)) continue; const run = async () => { - const runtime = source.runtime(plugin); - const common = createPluginCapabilities(plugin, runtime, source.navigation); + const common = createPluginCapabilities(plugin, source.navigation); try { if (item.context === "global") { await item.onSelect({ context: "global", ...common }); @@ -73,8 +70,6 @@ export function buildPluginCommandCenterContributions( }); } catch (error) { source.reportError(error); - } finally { - await runtime.paseo.dispose().catch(source.reportError); } }; contributions.push({ diff --git a/packages/app/src/plugins/command-center/registration.tsx b/packages/app/src/plugins/command-center/registration.tsx index cd85f9bff6..faf3b9215f 100644 --- a/packages/app/src/plugins/command-center/registration.tsx +++ b/packages/app/src/plugins/command-center/registration.tsx @@ -11,7 +11,6 @@ import { normalizeWorkspaceOpaqueId } from "@/utils/workspace-identity"; import { createPluginClientStateSource } from "../client-state/source"; import { hostIdFromPathname } from "../routes"; import { useInstalledPlugins } from "../registry"; -import { createPluginSurfaceRuntime } from "../surface-runtime"; import { buildPluginCommandCenterContributions } from "./contributions"; import { getFocusedAgentId } from "./context"; import { createPluginNavigation } from "../navigation"; @@ -53,11 +52,6 @@ export function PluginCommandCenterActions() { if (!client || !serverId || !stateSource) return []; return buildPluginCommandCenterContributions({ plugins, - runtime(plugin) { - const runtime = createPluginSurfaceRuntime(client, plugin); - if (!runtime) throw new Error("Plugin host is offline"); - return runtime; - }, state: stateSource, workspaceId: workspaceExists ? workspaceId : null, agentId: agentExists ? focusedAgentId : null, diff --git a/packages/app/src/plugins/contribution-host.ts b/packages/app/src/plugins/contribution-host.ts index cbead73be5..9614594802 100644 --- a/packages/app/src/plugins/contribution-host.ts +++ b/packages/app/src/plugins/contribution-host.ts @@ -7,3 +7,17 @@ export function getPreferredPluginContributionHost(contributionId: string): stri export function rememberPluginContributionHost(contributionId: string, serverId: string): void { preferredHosts.set(contributionId, serverId); } + +/** + * The host a plugin's screens and sidebar items share: the host last picked on one of its screens + * or last opened from one of its items. + */ +export function pluginScreensHostKey(pluginId: string): string { + return `${pluginId}/screens`; +} + +// COMPAT(pluginSidebarAliases): added in v0.11.0, remove after 2027-03-29 +/** The host an `addSidebarItem` row last opened, which its screen's host switcher also sets. */ +export function legacySidebarItemHostKey(pluginId: string, itemId: string): string { + return `${pluginId}/sidebar/${itemId}`; +} diff --git a/packages/app/src/plugins/evaluate.test.ts b/packages/app/src/plugins/evaluate.test.ts index 94f87d93ed..310ef5f7b9 100644 --- a/packages/app/src/plugins/evaluate.test.ts +++ b/packages/app/src/plugins/evaluate.test.ts @@ -5,6 +5,7 @@ const runtime = { paseo: {}, async rpc() {}, openSettings() {}, + openScreen() {}, openSurface() {}, openPanel() {}, addHeaderButton() { @@ -72,9 +73,10 @@ describe("evaluatePluginClientBundle", () => { function Component() { return null; } const schema = { safeParse(value) { return { success: true, data: value }; } }; globalThis.__pluginRemovals = [ - plugin.addSurface("main", Component), + plugin.addScreen({ id: "main", title: "Main", Component }), plugin.addSettingsScreen({ id: "display", title: "Display", icon: "Settings", Component }), - plugin.addSidebarItem({ id: "main", title: "Main", icon: "Blocks", surface: "main" }), + plugin.addSidebarHeaderItem({ id: "main", title: "Main", Component }), + plugin.addSidebarFooterItem({ id: "status", title: "Status", Component }), plugin.addWorkspacePanel({ id: "panel", title: "Panel", icon: "Blocks", context: "workspace", Component }), plugin.addCommandCenterItem({ id: "command", title: "Command", icon: "Blocks", context: "global", onSelect() {} }), plugin.addSlashCommand({ name: "review", description: "Review", argumentHint: "", context: "workspace", onSubmit() {} }), @@ -104,7 +106,8 @@ describe("evaluatePluginClientBundle", () => { [ plugin.surfaces, plugin.settingsScreens, - plugin.sidebarItems, + plugin.sidebarItems.header, + plugin.sidebarItems.footer, plugin.workspacePanels, plugin.commandCenterItems, plugin.clientSlashCommands, @@ -123,7 +126,8 @@ describe("evaluatePluginClientBundle", () => { [ plugin.surfaces, plugin.settingsScreens, - plugin.sidebarItems, + plugin.sidebarItems.header, + plugin.sidebarItems.footer, plugin.workspacePanels, plugin.commandCenterItems, plugin.clientSlashCommands, @@ -180,21 +184,125 @@ describe("evaluatePluginClientBundle", () => { ).toThrow("Timeline transformer bad-query has invalid item type: settled"); }); - it("collects a surface and its sidebar placement", () => { + it("collects screens and sidebar header and footer items", () => { + const plugin = evaluatePluginClientBundle( + "example", + bundle(` + function Screen() { return null; } + const Item = require("react").memo(function Item() { return null; }); + plugin.addScreen({ id: "main", title: " Main ", Component: Screen }); + plugin.addScreen({ id: "bot", title: (params) => params.botId, Component: Screen }); + plugin.addSidebarHeaderItem({ id: "main", title: " Example ", Component: Item }); + plugin.addSidebarFooterItem({ id: "main", title: "Status", Component: Item }); + `), + ); + + expect(plugin.id).toBe("example"); + expect(plugin.surfaces.map((surface) => surface.id)).toEqual(["main", "bot"]); + expect(plugin.surfaces[0]?.title).toBe("Main"); + expect(plugin.surfaces[1]?.title).toBeTypeOf("function"); + expect(plugin.sidebarItems.header.map(({ id, title }) => ({ id, title }))).toEqual([ + { id: "main", title: "Example" }, + ]); + expect(plugin.sidebarItems.footer.map(({ id, title }) => ({ id, title }))).toEqual([ + { id: "main", title: "Status" }, + ]); + expect(plugin.legacySidebarItems).toEqual([]); + }); + + it.each([ + [`plugin.addScreen("main", Screen);`, "addScreen takes { id, title, Component }"], + [`plugin.addScreen({ id: "main", Component: Screen });`, "Screen main needs a title"], + [ + `plugin.addScreen({ id: "main", title: " ", Component: Screen });`, + "Screen main needs a title", + ], + [ + `plugin.addScreen({ id: "main", title: 42, Component: Screen });`, + "Screen main needs a title", + ], + [ + `plugin.addScreen({ id: "main", title: "Main", Component: "Main" });`, + "Screen main is not a component", + ], + ])("rejects a malformed screen: %s", (call, message) => { + expect(() => + evaluatePluginClientBundle("example", bundle(`function Screen() { return null; } ${call}`)), + ).toThrow(message); + }); + + it("rejects duplicate and malformed sidebar items within a section", () => { + expect(() => + evaluatePluginClientBundle( + "example", + bundle(` + function Item() { return null; } + plugin.addSidebarFooterItem({ id: "main", title: "One", Component: Item }); + plugin.addSidebarFooterItem({ id: "main", title: "Two", Component: Item }); + `), + ), + ).toThrow("Duplicate sidebar footer item: main"); + expect(() => + evaluatePluginClientBundle( + "example", + bundle(`plugin.addSidebarHeaderItem({ id: "main", title: "Main", Component: "Main" });`), + ), + ).toThrow("Sidebar item main is not a component"); + expect(() => + evaluatePluginClientBundle( + "example", + bundle( + `plugin.addSidebarHeaderItem({ id: "main", title: " ", Component() { return null; } });`, + ), + ), + ).toThrow("Sidebar item main has no title"); + }); + + it("expands the addSurface and addSidebarItem aliases to a screen and a legacy header item", () => { const plugin = evaluatePluginClientBundle( "example", bundle(` function Surface() { return null; } plugin.addSurface("main", Surface); - plugin.addSidebarItem({ id: "main", title: "Example", icon: "Blocks", surface: "main" }); + plugin.addSidebarItem({ id: "entry", title: "Example", icon: "Blocks", surface: "main" }); `), ); - expect(plugin.id).toBe("example"); - expect(plugin.surfaces.map((surface) => surface.id)).toEqual(["main"]); - expect(plugin.sidebarItems).toEqual([ - { id: "main", title: "Example", icon: "Blocks", surface: "main" }, + expect(plugin.surfaces.map(({ id, title }) => ({ id, title }))).toEqual([ + { id: "main", title: "main" }, ]); + expect(plugin.sidebarItems).toEqual({ header: [], footer: [] }); + expect(plugin.legacySidebarItems).toEqual([ + { id: "entry", title: "Example", icon: "Blocks", surface: "main" }, + ]); + }); + + it("releases an addSidebarItem alias's header id", () => { + const plugin = evaluatePluginClientBundle( + "example", + bundle(` + plugin.addScreen({ id: "main", title: "Main", Component: function Screen() { return null; } }); + const remove = plugin.addSidebarItem({ id: "entry", title: "Example", icon: "Blocks", surface: "main" }); + remove(); + plugin.addSidebarHeaderItem({ id: "entry", title: "Replacement", Component() { return null; } }); + `), + ); + + expect(plugin.sidebarItems.header.map((item) => item.title)).toEqual(["Replacement"]); + expect(plugin.legacySidebarItems).toEqual([]); + }); + + it("rejects a header item that reuses an addSidebarItem id", () => { + expect(() => + evaluatePluginClientBundle( + "example", + bundle(` + plugin.addScreen({ id: "main", title: "Main", Component: function Screen() { return null; } }); + plugin.addSidebarItem({ id: "entry", title: "Example", icon: "Blocks", surface: "main" }); + plugin.addSidebarHeaderItem({ id: "entry", title: "Other", Component() { return null; } }); + `), + ), + ).toThrow("Duplicate sidebar header item: entry"); }); it("collects a declarative attachment source", () => { @@ -479,9 +587,9 @@ describe("evaluatePluginClientBundle", () => { const { Icon } = require("@getpaseo/plugin/client/react-native"); const module = { exports: {} }; module.exports.default = function(plugin) { - plugin.addSurface("main", function Surface() { + plugin.addScreen({ id: "main", title: "Main", Component: function Surface() { return Icon({ name: "Settings", size: 18, color: "#123456" }); - }); + } }); return function() {}; }; return module.exports; diff --git a/packages/app/src/plugins/evaluate.ts b/packages/app/src/plugins/evaluate.ts index b58f82b372..ecb8f89084 100644 --- a/packages/app/src/plugins/evaluate.ts +++ b/packages/app/src/plugins/evaluate.ts @@ -21,13 +21,17 @@ import { type PluginClientContext, type PluginClientSlashCommandContribution, type PluginSidebarContribution, + type PluginScreenContribution, + type PluginScreenProps, + type PluginScreenTitle, + type PluginSidebarItemContribution, type PluginSurfaceProps, type PluginTimelineRendererContribution, type PluginTimelineTransformerContribution, type PluginWorkspacePanelContribution, type PluginButtonRegistration, } from "@getpaseo/plugin/client"; -import type { EvaluatedPlugin } from "./types"; +import type { EvaluatedPlugin, PluginSidebarSection } from "./types"; import type { ComponentType } from "react"; import { resolvePluginIcon } from "./icons"; import { pluginReactNativeRuntime } from "./react-native/runtime"; @@ -65,17 +69,32 @@ function normalizePanelLocations( return normalized; } +function isComponentType(value: unknown): boolean { + return ( + typeof value === "function" || + (typeof value === "object" && value !== null && "$$typeof" in value) + ); +} + function requireId(value: string, label: string): string { const id = value.trim(); if (!CONTRIBUTION_ID.test(id)) throw new Error(`Invalid ${label}: ${value}`); return id; } +/** A non-empty string, or a function of the screen's params resolved when the header renders. */ +function requireScreenTitle(screenId: string, title: unknown): PluginScreenTitle { + if (typeof title === "function") return title as PluginScreenTitle; + if (typeof title === "string" && title.trim()) return title.trim(); + throw new Error(`Screen ${screenId} needs a title: a non-empty string or a function of params`); +} + export type PluginClientRuntime = Pick< PluginClientContext, | "paseo" | "rpc" | "openSettings" + | "openScreen" | "openSurface" | "openPanel" | "addComposerPill" @@ -91,7 +110,8 @@ export function runPluginClientBundle( const collector: Omit = { surfaces: [], settingsScreens: [], - sidebarItems: [], + sidebarItems: { header: [], footer: [] }, + legacySidebarItems: [], workspacePanels: [], commandCenterItems: [], clientSlashCommands: [], @@ -102,7 +122,10 @@ export function runPluginClientBundle( }; const surfaceIds = new Set(); const settingsScreenIds = new Set(); - const sidebarItemIds = new Set(); + const sidebarItemIds: Record> = { + header: new Set(), + footer: new Set(), + }; const workspacePanelIds = new Set(); const commandCenterItemIds = new Set(); const clientSlashCommandNames = new Set(); @@ -148,6 +171,26 @@ export function runPluginClientBundle( removals.add(remove); return remove; } + function addSidebarItem( + section: PluginSidebarSection, + contribution: PluginSidebarItemContribution, + ): PluginCleanup { + const ids = sidebarItemIds[section]; + const normalizedId = requireId(contribution.id, `sidebar ${section} item id`); + if (ids.has(normalizedId)) + throw new Error(`Duplicate sidebar ${section} item: ${normalizedId}`); + const title = contribution.title.trim(); + if (!title) throw new Error(`Sidebar item ${normalizedId} has no title`); + if (!isComponentType(contribution.Component)) { + throw new Error(`Sidebar item ${normalizedId} is not a component`); + } + ids.add(normalizedId); + return register( + collector.sidebarItems[section], + { id: normalizedId, title, Component: contribution.Component }, + () => ids.delete(normalizedId), + ); + } const pluginContext: PluginClientContext = { ...runtime, addSettingsScreen(contribution) { @@ -161,34 +204,56 @@ export function runPluginClientBundle( settingsScreenIds.delete(screenId), ); }, - addSurface(surfaceId: string, Component: ComponentType) { - const normalizedId = requireId(surfaceId, "surface id"); - if (surfaceIds.has(normalizedId)) throw new Error(`Duplicate surface: ${normalizedId}`); - if (typeof Component !== "function") - throw new Error(`Surface ${normalizedId} is not a component`); + addScreen(contribution: PluginScreenContribution) { + if (typeof contribution !== "object" || contribution === null) { + throw new Error("addScreen takes { id, title, Component }"); + } + const normalizedId = requireId(contribution.id, "screen id"); + if (surfaceIds.has(normalizedId)) throw new Error(`Duplicate screen: ${normalizedId}`); + const title = requireScreenTitle(normalizedId, contribution.title); + if (!isComponentType(contribution.Component)) { + throw new Error(`Screen ${normalizedId} is not a component`); + } surfaceIds.add(normalizedId); - return register(collector.surfaces, { id: normalizedId, Component }, () => - surfaceIds.delete(normalizedId), + return register( + collector.surfaces, + { id: normalizedId, title, Component: contribution.Component }, + () => surfaceIds.delete(normalizedId), ); }, + addSidebarHeaderItem(contribution: PluginSidebarItemContribution) { + return addSidebarItem("header", contribution); + }, + addSidebarFooterItem(contribution: PluginSidebarItemContribution) { + return addSidebarItem("footer", contribution); + }, + // COMPAT(pluginSidebarAliases): added in v0.11.0, remove after 2027-03-29 + // Titled by its id; a legacy item pointing at it lends the header its own title instead. + addSurface(surfaceId: string, Component: ComponentType) { + if (!isComponentType(Component)) throw new Error(`Screen ${surfaceId} is not a component`); + return pluginContext.addScreen({ + id: surfaceId, + title: surfaceId, + Component: (props: PluginScreenProps) => React.createElement(Component, props), + }); + }, + // COMPAT(pluginSidebarAliases): added in v0.11.0, remove after 2027-03-29 addSidebarItem(contribution: PluginSidebarContribution) { const normalizedId = requireId(contribution.id, "sidebar item id"); - if (sidebarItemIds.has(normalizedId)) - throw new Error(`Duplicate sidebar item: ${normalizedId}`); - if (!contribution.title.trim()) throw new Error(`Sidebar item ${normalizedId} has no title`); if (!contribution.icon.trim()) throw new Error(`Sidebar item ${normalizedId} has no icon`); resolvePluginIcon(contribution.icon.trim()); - sidebarItemIds.add(normalizedId); - return register( - collector.sidebarItems, - { - id: normalizedId, - title: contribution.title.trim(), - icon: contribution.icon.trim(), - surface: requireId(contribution.surface, "sidebar surface id"), - }, - () => sidebarItemIds.delete(normalizedId), - ); + const legacy = { + id: normalizedId, + title: contribution.title.trim(), + icon: contribution.icon.trim(), + surface: requireId(contribution.surface, "sidebar surface id"), + }; + // Shares the header's ids: the sidebar orders both kinds of item under one key. + const ids = sidebarItemIds.header; + if (ids.has(normalizedId)) throw new Error(`Duplicate sidebar header item: ${normalizedId}`); + if (!legacy.title) throw new Error(`Sidebar item ${normalizedId} has no title`); + ids.add(normalizedId); + return register(collector.legacySidebarItems, legacy, () => ids.delete(normalizedId)); }, addWorkspacePanel(contribution: PluginWorkspacePanelContribution) { const normalizedId = requireId(contribution.id, "workspace panel id"); @@ -411,7 +476,7 @@ export function runPluginClientBundle( entryCleanup = setup(pluginContext); if (typeof entryCleanup !== "function") throw new Error(`Plugin ${id} contribution must return a cleanup function`); - for (const item of collector.sidebarItems) { + for (const item of collector.legacySidebarItems) { if (!surfaceIds.has(item.surface)) { throw new Error(`Sidebar item ${item.id} references missing surface ${item.surface}`); } @@ -448,6 +513,7 @@ export function runPluginClientBundle( surfaces: collector.surfaces, settingsScreens: collector.settingsScreens, sidebarItems: collector.sidebarItems, + legacySidebarItems: collector.legacySidebarItems, workspacePanels: collector.workspacePanels as EvaluatedPlugin["workspacePanels"], commandCenterItems: collector.commandCenterItems, clientSlashCommands: collector.clientSlashCommands, diff --git a/packages/app/src/plugins/installation-provider.test.tsx b/packages/app/src/plugins/installation-provider.test.tsx new file mode 100644 index 0000000000..949bc7480f --- /dev/null +++ b/packages/app/src/plugins/installation-provider.test.tsx @@ -0,0 +1,71 @@ +import { usePaseo, useRpc } from "@getpaseo/plugin/client"; +import { defineRpc } from "@getpaseo/plugin"; +import type { PaseoApi } from "@getpaseo/client"; +import { QueryClient } from "@tanstack/react-query"; +import React from "react"; +import { renderToStaticMarkup } from "react-dom/server"; +import { expect, it } from "vitest"; +import { z } from "zod"; +import { PluginInstallationProvider } from "./installation-provider"; +import type { InstalledPlugin } from "./types"; + +const status = defineRpc({ + name: "deploys.status", + input: z.object({}), + output: z.object({ state: z.string() }), +}); + +function installation(invoked: string[]): InstalledPlugin { + return { + id: "deploys", + serverId: "host", + clientBundle: "", + lifetime: new AbortController(), + paseo: { dispose: async () => undefined } as PaseoApi, + invoke: async (method) => { + invoked.push(method); + return { state: "green" }; + }, + queryClient: new QueryClient(), + cleanup: () => undefined, + settingsScreens: [], + surfaces: [], + sidebarItems: { header: [], footer: [] }, + legacySidebarItems: [], + workspacePanels: [], + commandCenterItems: [], + clientSlashCommands: [], + attachmentSources: [], + themes: [], + timelineTransformers: [], + timelineRenderers: [], + }; +} + +it("gives every surface the installation's client and RPCs on its first frame", async () => { + const invoked: string[] = []; + const plugin = installation(invoked); + const received: PaseoApi[] = []; + let rpc: ((input: Record) => Promise) | null = null; + function Surface({ name }: { name: string }) { + received.push(usePaseo()); + rpc = useRpc(status); + return {name}; + } + + const markup = ["Screen", "Sidebar item", "Popover"] + .map((name) => + renderToStaticMarkup( + + + , + ), + ) + .join(""); + + expect(markup).toBe("ScreenSidebar itemPopover"); + expect(received).toEqual([plugin.paseo, plugin.paseo, plugin.paseo]); + expect(received.every((paseo) => paseo === plugin.paseo)).toBe(true); + await expect(rpc!({})).resolves.toEqual({ state: "green" }); + expect(invoked).toEqual(["deploys.status"]); +}); diff --git a/packages/app/src/plugins/installation-provider.tsx b/packages/app/src/plugins/installation-provider.tsx new file mode 100644 index 0000000000..40d06ef035 --- /dev/null +++ b/packages/app/src/plugins/installation-provider.tsx @@ -0,0 +1,21 @@ +import { QueryClientProvider } from "@tanstack/react-query"; +import { PaseoApiProvider, PluginRpcProvider } from "@getpaseo/plugin/client/host"; +import React, { type ReactNode } from "react"; +import type { InstalledPlugin } from "./types"; + +/** Every plugin surface renders under its installation: one query cache, one Paseo client, RPCs. */ +export function PluginInstallationProvider({ + plugin, + children, +}: { + plugin: InstalledPlugin; + children: ReactNode; +}) { + return ( + + + {children} + + + ); +} diff --git a/packages/app/src/plugins/navigation.ts b/packages/app/src/plugins/navigation.ts index 17e277504c..c1a50a98fc 100644 --- a/packages/app/src/plugins/navigation.ts +++ b/packages/app/src/plugins/navigation.ts @@ -23,8 +23,10 @@ export function createPluginNavigation(input: { openSettings(pluginId, screenId) { router.push(buildPluginSettingsRoute(serverId, pluginId, screenId)); }, - openSurface(pluginId, surfaceId) { - router.push(buildPluginSurfaceRoute(serverId, pluginId, { kind: "surface", id: surfaceId })); + openSurface(pluginId, surfaceId, params) { + router.push( + buildPluginSurfaceRoute(serverId, pluginId, { kind: "surface", id: surfaceId }, params), + ); }, openWorkspacePanel(pluginId, panelId, location) { if (!workspaceId) throw new Error("No active workspace"); diff --git a/packages/app/src/plugins/platform.ts b/packages/app/src/plugins/platform.ts new file mode 100644 index 0000000000..816095e255 --- /dev/null +++ b/packages/app/src/plugins/platform.ts @@ -0,0 +1,9 @@ +import type { PluginHostProps } from "@getpaseo/plugin/client"; +import { Platform } from "react-native"; + +/** The platform plugins see in `layout.platform`. */ +export function resolvePluginPlatform(): PluginHostProps["layout"]["platform"] { + if (Platform.OS === "ios") return "ios"; + if (Platform.OS === "android") return "android"; + return "web"; +} diff --git a/packages/app/src/plugins/popover.tsx b/packages/app/src/plugins/popover.tsx new file mode 100644 index 0000000000..f9680ebb29 --- /dev/null +++ b/packages/app/src/plugins/popover.tsx @@ -0,0 +1,59 @@ +import { PluginClientStateProvider } from "@getpaseo/plugin/client/host"; +import type { ReactNode } from "react"; +import { View } from "react-native"; +import { StyleSheet } from "react-native-unistyles"; +import { MenuSurface, type MenuSurfaceProps } from "@/components/ui/menu"; +import { ToastApiProvider, type useToast } from "@/contexts/toast-context"; +import type { createPluginClientStateSource } from "./client-state/source"; +import { PluginInstallationProvider } from "./installation-provider"; +import type { InstalledPlugin } from "./types"; + +export interface PluginEnvironment { + installation: InstalledPlugin; + state: ReturnType; + toast: ReturnType; +} + +interface PluginEnvironmentProviderProps { + environment: PluginEnvironment; + children: ReactNode; +} + +/** + * The providers plugin UI runs under. Render it around a contribution and again inside every + * surface it opens: native sheets teleport their children, so providers around the trigger alone + * cannot reach a popover body. + */ +export function PluginEnvironmentProvider({ + environment, + children, +}: PluginEnvironmentProviderProps) { + return ( + + + {children} + + + ); +} + +type PluginPopoverSurfaceProps = Pick< + MenuSurfaceProps, + "children" | "pages" | "sheetTitle" | "sheetTrailing" | "side" | "align" | "offset" | "testID" +>; + +/** + * The surface every plugin popover opens in: anchored to its trigger on wide layouts, a bottom + * sheet on compact ones when the enclosing `MenuRoot` uses `compactMode="sheet"`. + */ +export function PluginPopoverSurface(props: PluginPopoverSurfaceProps) { + return ; +} + +export function PluginPopoverContent({ children }: { children: ReactNode }) { + return {children}; +} + +const styles = StyleSheet.create((theme) => ({ + content: { padding: theme.spacing[3], gap: theme.spacing[2] }, +})); diff --git a/packages/app/src/plugins/react-native/ui.ts b/packages/app/src/plugins/react-native/ui.ts index 75ab40584c..1d7a72011f 100644 --- a/packages/app/src/plugins/react-native/ui.ts +++ b/packages/app/src/plugins/react-native/ui.ts @@ -10,3 +10,5 @@ export { } from "@/components/settings"; export { ExternalLink } from "@/components/ui/external-link"; + +export { SidebarRow, SidebarSeparator } from "@/plugins/sidebar-items/kit"; diff --git a/packages/app/src/plugins/registry-lifecycle.test.ts b/packages/app/src/plugins/registry-lifecycle.test.ts index 526ce1a809..445269c87e 100644 --- a/packages/app/src/plugins/registry-lifecycle.test.ts +++ b/packages/app/src/plugins/registry-lifecycle.test.ts @@ -1,16 +1,35 @@ import { createPluginHosts } from "./hosts"; +import type { DaemonClient } from "@getpaseo/client/internal/daemon-client"; +import type { PaseoApi } from "@getpaseo/client"; import { expect, test } from "vitest"; -import { createPaseoApi } from "@getpaseo/client"; -import { DaemonClient } from "@getpaseo/client/internal/daemon-client"; import { PluginRegistry } from "./registry"; +/** A host client that records open event observations by their first event name. */ +function observingClient() { + const open = new Set(); + const client = { + observeEvents(events: string[]) { + const [name] = events; + open.add(name); + return { + ready: Promise.resolve({ subscriptionId: name }), + subscribe: () => () => undefined, + release: async () => { + open.delete(name); + }, + }; + }, + invokePluginRpc: async () => null, + } as unknown as DaemonClient; + return { client, open }; +} + function registry() { - const client = new DaemonClient({ url: "ws://127.0.0.1:1/ws", clientId: "plugin-lifetime-test" }); - const released: string[] = []; + const setupClients = new Map(); const plugins = new PluginRegistry({ version: "0.8.0", createRuntime: (installation) => { - const api = createPaseoApi(client); + setupClients.set(installation.id, installation.paseo); return { hosts: createPluginHosts( { @@ -21,16 +40,11 @@ function registry() { }, installation.lifetime.signal, ), - paseo: { - ...api, - dispose: async () => { - released.push(installation.id); - await api.dispose(); - }, - }, + paseo: installation.paseo, rpc: async () => { throw new Error("Unexpected plugin RPC"); }, + openScreen: () => {}, openSurface: () => {}, openSettings: () => {}, openPanel: () => {}, @@ -39,36 +53,77 @@ function registry() { }; }, }); - return { client, released, plugins }; + return { ...observingClient(), plugins, setupClients }; } -function catalog(id: string, body: string) { +/** A plugin whose setup opens an observation named after it and never releases it. */ +function catalog(id: string, body = "return function() {};") { return { id, requirements: { paseo: ">=0.8.0" }, - clientBundle: `(function() { return { default: function(plugin) { ${body} } }; })`, + clientBundle: `(function() { return { default: function(plugin) { plugin.paseo.observeEvents(["${id}"]); ${body} } }; })`, }; } -test("failed plugin initialization disposes its API scope", async () => { +test("setup and every surface share the installation's one Paseo client", () => { + const h = registry(); + h.plugins.installCatalog("host", [catalog("deploys")], { client: h.client }); + + const [installation] = h.plugins.getSnapshot(); + expect(h.setupClients.get("deploys")).toBe(installation.paseo); + expect(h.open).toEqual(new Set(["deploys"])); +}); + +test("teardown ends every subscription the plugin still holds", async () => { + const h = registry(); + h.plugins.installCatalog("host", [catalog("disabled"), catalog("kept")], { client: h.client }); + expect(h.open).toEqual(new Set(["disabled", "kept"])); + + // Disabling one plugin removes it from the catalog. + h.plugins.installCatalog("host", [catalog("kept")], { client: h.client }); + await expect.poll(() => [...h.open]).toEqual(["kept"]); + + h.plugins.removeHost("host"); + await expect.poll(() => h.open.size).toBe(0); +}); + +test("reloading a plugin ends the old installation's subscriptions", async () => { + const h = registry(); + h.plugins.installCatalog("host", [catalog("reloaded")], { client: h.client }); + const [first] = h.plugins.getSnapshot(); + + h.plugins.installCatalog("host", [catalog("reloaded")], { + client: h.client, + replacePluginId: "reloaded", + }); + + const [second] = h.plugins.getSnapshot(); + expect(second).not.toBe(first); + expect(second.paseo).not.toBe(first.paseo); + await expect(first.paseo.dispose()).resolves.toBeUndefined(); + expect(() => first.paseo.observeEvents(["project.update"])).toThrow("Paseo API is disposed"); + expect(h.open).toEqual(new Set(["reloaded"])); +}); + +test("failed plugin initialization ends the subscriptions it opened", async () => { const h = registry(); h.plugins.installCatalog("host", [catalog("failed", 'throw new Error("setup failed");')], { client: h.client, }); - await expect.poll(() => h.released).toEqual(["failed"]); + await expect.poll(() => h.open.size).toBe(0); expect(h.plugins.getSnapshot()).toEqual([]); }); -test("unloading releases the API even when plugin cleanup throws, preserving another plugin", async () => { +test("unloading ends the subscriptions even when plugin cleanup throws, preserving another plugin", async () => { const h = registry(); const failing = catalog("failing", 'return function() { throw new Error("cleanup failed"); };'); const surviving = catalog("surviving", "return function() {};"); h.plugins.installCatalog("host", [failing, surviving], { client: h.client }); h.plugins.installCatalog("host", [surviving], { client: h.client }); - await expect.poll(() => h.released).toEqual(["failing"]); + await expect.poll(() => [...h.open]).toEqual(["surviving"]); expect(h.plugins.getSnapshot().map((plugin) => plugin.id)).toEqual(["surviving"]); h.plugins.removeHost("host"); - await expect.poll(() => h.released).toEqual(["failing", "surviving"]); + await expect.poll(() => h.open.size).toBe(0); }); test("an invalid async client entry is disposed and its rejected continuation is observed", async () => { @@ -78,7 +133,7 @@ test("an invalid async client entry is disposed and its rejected continuation is [catalog("async-entry", 'return Promise.reject(new Error("asynchronous setup failed"));')], { client: h.client }, ); - await expect.poll(() => h.released).toEqual(["async-entry"]); + await expect.poll(() => h.open.size).toBe(0); await new Promise((resolve) => setTimeout(resolve, 0)); expect(h.plugins.getSnapshot()).toEqual([]); }); diff --git a/packages/app/src/plugins/registry-requirements.test.ts b/packages/app/src/plugins/registry-requirements.test.ts index 8f096aeac9..055fe207cd 100644 --- a/packages/app/src/plugins/registry-requirements.test.ts +++ b/packages/app/src/plugins/registry-requirements.test.ts @@ -1,6 +1,5 @@ import { createPluginHosts } from "./hosts"; import { afterEach, expect, it } from "vitest"; -import { createPaseoApi } from "@getpaseo/client"; import { DaemonClient } from "@getpaseo/client/internal/daemon-client"; import { PluginRegistry } from "./registry"; @@ -23,13 +22,14 @@ function registry(version: string) { }, installation.lifetime.signal, ), - paseo: createPaseoApi(client), + paseo: installation.paseo, rpc: async () => { throw new Error("No RPC in this plugin"); }, openSettings() { cleanups++; }, + openScreen() {}, openSurface() {}, openPanel() {}, addComposerPill: () => ({ update() {}, remove() {} }), diff --git a/packages/app/src/plugins/registry.ts b/packages/app/src/plugins/registry.ts index 2894784fc7..eed76a530f 100644 --- a/packages/app/src/plugins/registry.ts +++ b/packages/app/src/plugins/registry.ts @@ -1,10 +1,11 @@ import { useMemo, useSyncExternalStore } from "react"; import { QueryClient } from "@tanstack/react-query"; +import { createPaseoApi } from "@getpaseo/client"; import type { DaemonClient } from "@getpaseo/client/internal/daemon-client"; import { assertPluginCompatibility } from "@getpaseo/protocol/plugin-requirements"; import { resolveAppVersion } from "@/utils/app-version"; import { createPluginClientRuntime } from "./client-runtime"; -import { runPluginClientBundle, type PluginClientRuntime } from "./evaluate"; +import { runPluginClientBundle } from "./evaluate"; import type { InstalledPlugin } from "./types"; type CatalogPlugin = Awaited>[number]; @@ -64,8 +65,7 @@ export class PluginRegistry { } const installed = catalog.flatMap((entry) => { const key = `${serverId}/${entry.id}`; - let runtime: PluginClientRuntime | undefined; - let lifetime: AbortController | undefined; + let installation: InstalledPlugin | undefined; try { if (!entry.clientBundle) return []; assertPluginCompatibility({ ...entry, version: this.dependencies.version, runtime: "app" }); @@ -76,9 +76,11 @@ export class PluginRegistry { this.evaluationErrors.delete(key); return [existing]; } - lifetime = new AbortController(); - const installation: InstalledPlugin = { - lifetime, + const client = options.client; + installation = { + lifetime: new AbortController(), + paseo: createPaseoApi(client), + invoke: (method, input) => client.invokePluginRpc(entry.id, method, input), id: entry.id, serverId, clientBundle: entry.clientBundle, @@ -87,7 +89,8 @@ export class PluginRegistry { cleanup: () => undefined, surfaces: [], settingsScreens: [], - sidebarItems: [], + sidebarItems: { header: [], footer: [] }, + legacySidebarItems: [], workspacePanels: [], commandCenterItems: [], clientSlashCommands: [], @@ -96,12 +99,12 @@ export class PluginRegistry { timelineTransformers: [], timelineRenderers: [], }; - runtime = this.dependencies.createRuntime(installation, options.client); + const runtime = this.dependencies.createRuntime(installation); const evaluated = runPluginClientBundle(entry.id, entry.clientBundle, runtime, () => this.publish(), ); Object.assign(installation, evaluated); - const paseo = runtime.paseo; + const paseo = installation.paseo; installation.cleanup = async () => { const results = await Promise.allSettled([paseo.dispose(), evaluated.cleanup()]); const failures = results.filter((result) => result.status === "rejected"); @@ -114,8 +117,8 @@ export class PluginRegistry { this.evaluationErrors.delete(key); return [installation]; } catch (error) { - lifetime?.abort(); - void runtime?.paseo + installation?.lifetime.abort(); + void installation?.paseo .dispose() .catch((failure) => console.warn(`[Plugins] API cleanup failed for ${key}`, failure)); this.evaluationErrors.set(key, error instanceof Error ? error.message : String(error)); diff --git a/packages/app/src/plugins/routes.test.ts b/packages/app/src/plugins/routes.test.ts index 18c6bfb1a0..b05b55da6b 100644 --- a/packages/app/src/plugins/routes.test.ts +++ b/packages/app/src/plugins/routes.test.ts @@ -1,5 +1,10 @@ import { describe, expect, it } from "vitest"; -import { buildLegacyPluginSurfaceRedirectRoute, buildPluginSurfaceRoute } from "./routes"; +import { + buildLegacyPluginSurfaceRedirectRoute, + buildPluginSurfaceRoute, + parsePluginSurfaceRoute, + pluginScreenParamsFromRoute, +} from "./routes"; describe("buildPluginSurfaceRoute", () => { it("keeps direct surfaces and sidebar contributions in separate route namespaces", () => { @@ -11,9 +16,79 @@ describe("buildPluginSurfaceRoute", () => { ); }); + it("puts screen params in the query under their own prefix, encoded", () => { + expect( + buildPluginSurfaceRoute( + "local", + "bots", + { kind: "surface", id: "bot" }, + { botId: "bot 2", "a&b": "x=y" }, + ), + ).toBe("/h/local/plugin/bots/surface/bot?param.botId=bot%202¶m.a%26b=x%3Dy"); + expect(buildPluginSurfaceRoute("local", "bots", { kind: "surface", id: "bot" }, {})).toBe( + "/h/local/plugin/bots/surface/bot", + ); + }); + it("redirects legacy plugin surface URLs to their sidebar contribution identity", () => { expect(buildLegacyPluginSurfaceRedirectRoute("host/one", "review", "overview/item")).toBe( "/h/host%2Fone/plugin/review/sidebar/overview%2Fitem", ); }); }); + +describe("parsePluginSurfaceRoute", () => { + it("reads both route namespaces back from the paths buildPluginSurfaceRoute produces", () => { + expect( + parsePluginSurfaceRoute( + buildPluginSurfaceRoute("host/one", "review", { kind: "surface", id: "overview" }), + ), + ).toEqual({ + serverId: "host/one", + pluginId: "review", + identity: { kind: "surface", id: "overview" }, + }); + expect(parsePluginSurfaceRoute("/h/local/plugin/review/sidebar/item")).toEqual({ + serverId: "local", + pluginId: "review", + identity: { kind: "sidebar", id: "item" }, + }); + }); + + it("ignores routes that are not plugin screens", () => { + expect(parsePluginSurfaceRoute("/h/local/workspace/abc")).toBeNull(); + expect(parsePluginSurfaceRoute("/h/local/plugin/review/settings/general")).toBeNull(); + }); +}); + +describe("pluginScreenParamsFromRoute", () => { + function searchParams(route: string): Record { + // Expo Router's search params: the route's segments merged with its query. + const url = new URL(route, "http://paseo.test"); + return { + serverId: "local", + pluginId: "bots", + contributionKind: "surface", + contributionId: "bot", + ...Object.fromEntries(url.searchParams), + }; + } + + it("round-trips params whose keys match the route's own segments", () => { + const params = { serverId: "x", pluginId: "y", botId: "bot-2" }; + const route = buildPluginSurfaceRoute("local", "bots", { kind: "surface", id: "bot" }, params); + expect(pluginScreenParamsFromRoute(searchParams(route))).toEqual(params); + }); + + it("ignores the route's segments and non-string values", () => { + expect( + pluginScreenParamsFromRoute({ + serverId: "local", + contributionId: "bot", + "param.botId": "bot-2", + "param.tags": ["a", "b"], + "param.missing": undefined, + }), + ).toEqual({ botId: "bot-2" }); + }); +}); diff --git a/packages/app/src/plugins/routes.ts b/packages/app/src/plugins/routes.ts index d4fbf2ebef..3c51fe4a3b 100644 --- a/packages/app/src/plugins/routes.ts +++ b/packages/app/src/plugins/routes.ts @@ -1,16 +1,58 @@ +import type { PluginScreenParams } from "@getpaseo/plugin/client"; import type { PluginSurfaceContributionIdentity } from "./surface-contribution"; type PluginSurfaceRoute = `/h/${string}/plugin/${string}/${Kind}/${string}`; +/** + * Expo Router merges the route's segments (`serverId`, `pluginId`, ...) into the same search params + * as the query. Prefixing screen param keys keeps any plugin key clear of them. + */ +const SCREEN_PARAM_PREFIX = "param."; + +/** Screen params ride in the route's query, so reload, history and deep links keep them. */ export function buildPluginSurfaceRoute( serverId: string, pluginId: string, identity: Identity, + params: PluginScreenParams = {}, ): PluginSurfaceRoute { - return `/h/${encodeURIComponent(serverId)}/plugin/${encodeURIComponent(pluginId)}/${identity.kind}/${encodeURIComponent(identity.id)}` as PluginSurfaceRoute< - Identity["kind"] - >; + const path = `/h/${encodeURIComponent(serverId)}/plugin/${encodeURIComponent(pluginId)}/${identity.kind}/${encodeURIComponent(identity.id)}`; + const query = Object.entries(params) + .map( + ([key, value]) => + `${encodeURIComponent(SCREEN_PARAM_PREFIX + key)}=${encodeURIComponent(value)}`, + ) + .join("&"); + return (query ? `${path}?${query}` : path) as PluginSurfaceRoute; +} + +/** Checks params a plugin passed to `openScreen`: an object of string values. Throws otherwise. */ +export function parsePluginScreenParams(value: unknown): PluginScreenParams { + if (value === undefined) return {}; + if (typeof value !== "object" || value === null || Array.isArray(value)) { + throw new Error("Plugin screen params must be an object of strings"); + } + const params: PluginScreenParams = {}; + for (const [key, entry] of Object.entries(value)) { + if (typeof entry !== "string") { + throw new Error(`Plugin screen param ${key} must be a string`); + } + params[key] = entry; + } + return params; +} + +/** The screen params in a plugin screen route's search params (`useLocalSearchParams`). */ +export function pluginScreenParamsFromRoute( + routeParams: Readonly>, +): PluginScreenParams { + const params: PluginScreenParams = {}; + for (const [key, value] of Object.entries(routeParams)) { + if (!key.startsWith(SCREEN_PARAM_PREFIX) || typeof value !== "string") continue; + params[key.slice(SCREEN_PARAM_PREFIX.length)] = value; + } + return params; } export function buildLegacyPluginSurfaceRedirectRoute( @@ -33,3 +75,21 @@ export function hostIdFromPathname(pathname: string): string | null { return null; } } + +export function parsePluginSurfaceRoute(pathname: string): { + serverId: string; + pluginId: string; + identity: PluginSurfaceContributionIdentity; +} | null { + const match = /^\/h\/([^/]+)\/plugin\/([^/]+)\/(sidebar|surface)\/([^/]+)\/?$/.exec(pathname); + if (!match) return null; + try { + return { + serverId: decodeURIComponent(match[1]), + pluginId: decodeURIComponent(match[2]), + identity: { kind: match[3] as "sidebar" | "surface", id: decodeURIComponent(match[4]) }, + }; + } catch { + return null; + } +} diff --git a/packages/app/src/plugins/runtime-boundary.tsx b/packages/app/src/plugins/runtime-boundary.tsx deleted file mode 100644 index 7ae51af2b9..0000000000 --- a/packages/app/src/plugins/runtime-boundary.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { QueryClientProvider } from "@tanstack/react-query"; -import { PaseoApiProvider, PluginRpcProvider } from "@getpaseo/plugin/client/host"; -import type { ReactNode } from "react"; -import type { InstalledPlugin } from "./types"; -import { usePluginSurfaceRuntime } from "./surface-runtime"; -import type { DaemonClient } from "@getpaseo/client/internal/daemon-client"; - -export function PluginRuntimeBoundary({ - plugin, - client, - children, -}: { - plugin: InstalledPlugin; - client: DaemonClient; - children: ReactNode; -}) { - const runtime = usePluginSurfaceRuntime(client, plugin); - if (!runtime) return null; - return ( - - - {children} - - - ); -} diff --git a/packages/app/src/plugins/settings/index.tsx b/packages/app/src/plugins/settings/index.tsx index 11b5c199df..0fda205671 100644 --- a/packages/app/src/plugins/settings/index.tsx +++ b/packages/app/src/plugins/settings/index.tsx @@ -12,7 +12,7 @@ import { useHostFeature } from "@/runtime/host-features"; import { useHostRuntimeClient, useHostRuntimeIsConnected, useHosts } from "@/runtime/host-runtime"; import type { Theme } from "@/styles/theme"; import { useInstalledPlugin } from "../registry"; -import { PluginRuntimeBoundary } from "../runtime-boundary"; +import { PluginInstallationProvider } from "../installation-provider"; import { SurfaceErrorBoundary } from "../surface-error-boundary"; import { toPluginTheme } from "../theme"; import { buildPluginSettingsRoute } from "./routes"; @@ -119,14 +119,15 @@ function SettingsContent({ resetKey={attempt} renderError={renderError} > - + - + ); } const ThemedSettingsContent = withUnistyles(SettingsContent); +// Settings routes carry no screen params. const themeMapping = (theme: Theme) => ({ theme: toPluginTheme(theme) }); export function PluginSettingsContent({ onBackToPlugins, diff --git a/packages/app/src/plugins/sidebar-groups.test.ts b/packages/app/src/plugins/sidebar-groups.test.ts index 7a38bb8eb5..314e5fe1e7 100644 --- a/packages/app/src/plugins/sidebar-groups.test.ts +++ b/packages/app/src/plugins/sidebar-groups.test.ts @@ -1,26 +1,35 @@ import { QueryClient } from "@tanstack/react-query"; import { describe, expect, it } from "vitest"; import type { InstalledPlugin } from "./types"; -import { groupPluginSidebarContributions } from "./sidebar-groups"; +import { + legacySidebarItemHostKey, + pluginScreensHostKey, + rememberPluginContributionHost, +} from "./contribution-host"; +import { groupPluginSidebarItems, selectPluginSidebarTarget } from "./sidebar-groups"; -function installed(serverId: string, contributionId = "main"): InstalledPlugin { +function installed( + serverId: string, + contributionId = "main", + legacySidebarItems: InstalledPlugin["legacySidebarItems"] = [], + pluginId = "example", +): InstalledPlugin { return { - id: "example", + id: pluginId, cleanup: () => undefined, serverId, clientBundle: serverId, lifetime: new AbortController(), queryClient: new QueryClient(), + paseo: {} as InstalledPlugin["paseo"], + invoke: async () => undefined, settingsScreens: [], - surfaces: [{ id: "surface", Component: () => null }], - sidebarItems: [ - { - id: contributionId, - title: "Example", - icon: "Blocks", - surface: "surface", - }, - ], + surfaces: [{ id: "surface", title: "Surface", Component: () => null }], + sidebarItems: { + header: [{ id: contributionId, title: "Example", Component: () => null }], + footer: [{ id: "status", title: "Status", Component: () => null }], + }, + legacySidebarItems, workspacePanels: [], commandCenterItems: [], clientSlashCommands: [], @@ -31,9 +40,9 @@ function installed(serverId: string, contributionId = "main"): InstalledPlugin { }; } -describe("groupPluginSidebarContributions", () => { +describe("groupPluginSidebarItems", () => { it("coalesces the same plugin contribution across hosts", () => { - const groups = groupPluginSidebarContributions([installed("host-a"), installed("host-b")]); + const groups = groupPluginSidebarItems([installed("host-a"), installed("host-b")], "header"); expect(groups).toHaveLength(1); expect(groups[0]?.targets.map((target) => target.plugin.serverId)).toEqual([ @@ -43,14 +52,66 @@ describe("groupPluginSidebarContributions", () => { }); it("keeps different contribution ids separate", () => { - const groups = groupPluginSidebarContributions([ - installed("host-a", "main"), - installed("host-b", "settings"), - ]); + const groups = groupPluginSidebarItems( + [installed("host-a", "main"), installed("host-b", "settings")], + "header", + ); expect(groups.map((group) => group.key)).toEqual([ "example/sidebar/main", "example/sidebar/settings", ]); }); + + it("keeps a legacy addSidebarItem as its own kind, with its icon, in the header only", () => { + const legacy = [{ id: "entry", title: "Legacy", icon: "Blocks", surface: "surface" }]; + const plugins = [installed("host-a", "main", legacy), installed("host-b", "main", legacy)]; + const [legacyGroup, itemGroup] = groupPluginSidebarItems(plugins, "header"); + + expect(legacyGroup).toMatchObject({ kind: "legacy", title: "Legacy", icon: "Blocks" }); + expect(legacyGroup?.targets.map((target) => target.plugin.serverId)).toEqual([ + "host-a", + "host-b", + ]); + expect(itemGroup?.kind).toBe("item"); + expect(groupPluginSidebarItems(plugins, "footer").map((group) => group.kind)).toEqual(["item"]); + }); + + it("groups only the requested section", () => { + const groups = groupPluginSidebarItems([installed("host-a")], "footer"); + + expect(groups.map((group) => group.key)).toEqual(["example/sidebar-footer/status"]); + }); +}); + +describe("selectPluginSidebarTarget", () => { + const legacy = [{ id: "entry", title: "Legacy", icon: "Blocks", surface: "surface" }]; + + function hostsOf(section: "header" | "footer", pluginId: string) { + const plugins = ["host-a", "host-b"].map((serverId) => + installed(serverId, "main", legacy, pluginId), + ); + return groupPluginSidebarItems(plugins, section).map( + (group) => selectPluginSidebarTarget(group, null).plugin.serverId, + ); + } + + it("prefers the current route's host", () => { + const [group] = groupPluginSidebarItems([installed("host-a"), installed("host-b")], "footer"); + expect(selectPluginSidebarTarget(group!, "host-b").plugin.serverId).toBe("host-b"); + }); + + it("carries the host picked on any of a plugin's screens to all its items, but not legacy rows", () => { + expect(hostsOf("header", "screens")).toEqual(["host-a", "host-a"]); + rememberPluginContributionHost(pluginScreensHostKey("screens"), "host-b"); + // Header: the legacy row keeps its own host; the item follows the screens' host. + expect(hostsOf("header", "screens")).toEqual(["host-a", "host-b"]); + expect(hostsOf("footer", "screens")).toEqual(["host-b"]); + expect(hostsOf("footer", "other")).toEqual(["host-a"]); + }); + + it("keeps a legacy row's host under its own key", () => { + rememberPluginContributionHost(legacySidebarItemHostKey("legacy-row", "entry"), "host-b"); + expect(hostsOf("header", "legacy-row")).toEqual(["host-b", "host-a"]); + }); }); diff --git a/packages/app/src/plugins/sidebar-groups.ts b/packages/app/src/plugins/sidebar-groups.ts index 05c868f283..c764a53dcc 100644 --- a/packages/app/src/plugins/sidebar-groups.ts +++ b/packages/app/src/plugins/sidebar-groups.ts @@ -1,29 +1,65 @@ -import type { InstalledPlugin, PluginSidebarContribution } from "./types"; +import { + getPreferredPluginContributionHost, + legacySidebarItemHostKey, + pluginScreensHostKey, +} from "./contribution-host"; +import type { + InstalledPlugin, + PluginSidebarContribution, + PluginSidebarItemContribution, + PluginSidebarSection, +} from "./types"; export interface PluginSidebarTarget { + plugin: InstalledPlugin; + item: PluginSidebarItemContribution; +} + +// COMPAT(pluginSidebarAliases): added in v0.11.0, remove after 2027-03-29 +export interface PluginLegacySidebarTarget { plugin: InstalledPlugin; item: PluginSidebarContribution; } -export interface PluginSidebarGroup { +interface PluginSidebarGroupBase { key: string; pluginId: string; contributionId: string; title: string; - icon: string; - targets: PluginSidebarTarget[]; } -export function groupPluginSidebarContributions(plugins: InstalledPlugin[]): PluginSidebarGroup[] { +/** + * One sidebar item, coalesced across every host that contributes it. A `legacy` group is an + * `addSidebarItem` row: the app draws it with its registered icon and it opens its sidebar route. + */ +export type PluginSidebarGroup = + | (PluginSidebarGroupBase & { kind: "item"; targets: PluginSidebarTarget[] }) + | (PluginSidebarGroupBase & { + kind: "legacy"; + icon: string; + targets: PluginLegacySidebarTarget[]; + }); + +function groupKey(pluginId: string, section: PluginSidebarSection, itemId: string) { + return section === "header" + ? legacySidebarItemHostKey(pluginId, itemId) + : `${pluginId}/sidebar-${section}/${itemId}`; +} + +export function groupPluginSidebarItems( + plugins: InstalledPlugin[], + section: PluginSidebarSection, +): PluginSidebarGroup[] { const groups = new Map(); for (const plugin of plugins) { - for (const item of plugin.sidebarItems) { - const key = `${plugin.id}/sidebar/${item.id}`; + const legacyItems = section === "header" ? plugin.legacySidebarItems : []; + for (const item of legacyItems) { + const key = groupKey(plugin.id, section, item.id); const existing = groups.get(key); - if (existing) { - existing.targets.push({ plugin, item }); - } else { + if (existing?.kind === "legacy") existing.targets.push({ plugin, item }); + else if (!existing) { groups.set(key, { + kind: "legacy", key, pluginId: plugin.id, contributionId: item.id, @@ -33,6 +69,41 @@ export function groupPluginSidebarContributions(plugins: InstalledPlugin[]): Plu }); } } + for (const item of plugin.sidebarItems[section]) { + const key = groupKey(plugin.id, section, item.id); + const existing = groups.get(key); + if (existing?.kind === "item") existing.targets.push({ plugin, item }); + else if (!existing) { + groups.set(key, { + kind: "item", + key, + pluginId: plugin.id, + contributionId: item.id, + title: item.title, + targets: [{ plugin, item }], + }); + } + } } return [...groups.values()]; } + +/** + * The host a group's row uses: the current route's host, else the remembered one, else the first. + * A legacy row remembers its own host; other items share the host last used with the plugin's + * screens. + */ +export function selectPluginSidebarTarget( + group: Group, + currentHostId: string | null, +): Group["targets"][number] { + const rememberedHostId = getPreferredPluginContributionHost( + group.kind === "legacy" ? group.key : pluginScreensHostKey(group.pluginId), + ); + const targets: Group["targets"] = group.targets; + return ( + targets.find((target) => target.plugin.serverId === currentHostId) ?? + targets.find((target) => target.plugin.serverId === rememberedHostId) ?? + targets[0] + ); +} diff --git a/packages/app/src/plugins/sidebar-items.tsx b/packages/app/src/plugins/sidebar-items.tsx deleted file mode 100644 index 97c0c6fa6d..0000000000 --- a/packages/app/src/plugins/sidebar-items.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { router, usePathname } from "expo-router"; -import { useCallback } from "react"; -import { SidebarHeaderRow } from "@/components/sidebar/sidebar-header-row"; -import { resolvePluginIcon } from "./icons"; -import { buildPluginSurfaceRoute, hostIdFromPathname } from "./routes"; -import { - getPreferredPluginContributionHost, - rememberPluginContributionHost, -} from "./contribution-host"; -import { type PluginSidebarGroup, type PluginSidebarTarget } from "./sidebar-groups"; - -function selectTarget( - group: PluginSidebarGroup, - currentHostId: string | null, -): PluginSidebarTarget { - const current = group.targets.find((target) => target.plugin.serverId === currentHostId); - if (current) return current; - const rememberedHostId = getPreferredPluginContributionHost(group.key); - const remembered = group.targets.find((target) => target.plugin.serverId === rememberedHostId); - return remembered ?? group.targets[0]; -} - -export function PluginSidebarItemRow({ - group, - onBeforeNavigate, -}: { - group: PluginSidebarGroup; - onBeforeNavigate?: () => void; -}) { - const pathname = usePathname(); - const target = selectTarget(group, hostIdFromPathname(pathname)); - const route = buildPluginSurfaceRoute(target.plugin.serverId, group.pluginId, { - kind: "sidebar", - id: group.contributionId, - }); - const isActive = group.targets.some( - (candidate) => - pathname === - buildPluginSurfaceRoute(candidate.plugin.serverId, group.pluginId, { - kind: "sidebar", - id: group.contributionId, - }), - ); - const navigate = useCallback(() => { - rememberPluginContributionHost(group.key, target.plugin.serverId); - onBeforeNavigate?.(); - router.push(route); - }, [group.key, onBeforeNavigate, route, target.plugin.serverId]); - return ( - - ); -} diff --git a/packages/app/src/plugins/sidebar-items/frame.ts b/packages/app/src/plugins/sidebar-items/frame.ts new file mode 100644 index 0000000000..f5f42e5d56 --- /dev/null +++ b/packages/app/src/plugins/sidebar-items/frame.ts @@ -0,0 +1,23 @@ +import { createContext, useContext } from "react"; +import type { View } from "react-native"; +import type { PluginSidebarSection } from "../types"; + +/** What the host tells the kit components about the sidebar item they render in. */ +export interface SidebarItemFrame { + section: PluginSidebarSection; + title: string; + testID: string; + /** A row calls this before its `onPress`, so a popover that press opens anchors to it. */ + anchorTo: (node: View | null) => void; + /** Rows offer themselves on mount, so a popover opened without a press anchors to a row. */ + offerAnchor: (node: View | null) => void; + releaseAnchor: (node: View | null) => void; +} + +export const SidebarItemFrameContext = createContext(null); + +export function useSidebarItemFrame(componentName: string): SidebarItemFrame { + const frame = useContext(SidebarItemFrameContext); + if (!frame) throw new Error(`${componentName} must render inside a sidebar item`); + return frame; +} diff --git a/packages/app/src/plugins/sidebar-items/index.tsx b/packages/app/src/plugins/sidebar-items/index.tsx new file mode 100644 index 0000000000..11657c5d35 --- /dev/null +++ b/packages/app/src/plugins/sidebar-items/index.tsx @@ -0,0 +1,245 @@ +import type { + PluginHostProps, + PluginOpenScreenInput, + PluginPopoverProps, + PluginScreenLocation, + PluginSidebarItemProps, +} from "@getpaseo/plugin/client"; +import type { PluginTheme } from "@getpaseo/plugin"; +import { router, useGlobalSearchParams, usePathname } from "expo-router"; +import { useCallback, useMemo, useState, type ComponentType, type RefObject } from "react"; +import { type View } from "react-native"; +import { withUnistyles } from "react-native-unistyles"; +import { + SidebarPopoverRoot, + SidebarPopoverSurface, + useSidebarPopoverAnchor, +} from "@/components/sidebar/sidebar-popover"; +import { useIsCompactFormFactor } from "@/constants/layout"; +import { useToast } from "@/contexts/toast-context"; +import { useHostRuntimeClient, useHosts } from "@/runtime/host-runtime"; +import type { Theme } from "@/styles/theme"; +import { createPluginClientStateSource } from "../client-state/source"; +import { pluginScreensHostKey, rememberPluginContributionHost } from "../contribution-host"; +import { + PluginEnvironmentProvider, + PluginPopoverContent, + type PluginEnvironment, +} from "../popover"; +import { buildPluginSurfaceRoute, hostIdFromPathname } from "../routes"; +import { + selectPluginSidebarTarget, + type PluginSidebarGroup, + type PluginSidebarTarget, +} from "../sidebar-groups"; +import { currentPluginScreen, parsePluginOpenScreenInput } from "../surface-contribution"; +import { SurfaceErrorBoundary } from "../surface-error-boundary"; +import { toPluginTheme } from "../theme"; +import type { PluginSidebarSection } from "../types"; +import { SidebarItemFrameContext, type SidebarItemFrame } from "./frame"; +import { LegacyPluginSidebarRow } from "./legacy"; +import { resolvePluginPlatform } from "../platform"; + +export { SidebarRow, SidebarSeparator } from "./kit"; + +type PopoverContent = ComponentType; + +const pluginThemeMapping = (theme: Theme) => ({ theme: toPluginTheme(theme) }); + +type PluginItemGroup = Extract; + +function renderNothing() { + return null; +} + +interface PluginSidebarItemEntryProps { + group: PluginSidebarGroup; + section: PluginSidebarSection; + /** Anchors a popover opened by an item that renders no kit component. */ + fallbackAnchorRef: RefObject; + onBeforeNavigate?: () => void; +} + +type PluginSidebarItemHostProps = Omit & { + group: PluginItemGroup; +}; + +/** + * One plugin sidebar item. Renders the item's `Component` from the host `selectPluginSidebarTarget` picks, + * inside the plugin's runtime and its own error boundary: a throwing item renders nothing. A legacy + * `addSidebarItem` group renders the app's own row. + */ +export function PluginSidebarItem({ group, ...props }: PluginSidebarItemEntryProps) { + if (group.kind === "legacy") { + return ; + } + return ; +} + +function PluginSidebarItemHost({ + group, + section, + fallbackAnchorRef, + onBeforeNavigate, + theme, +}: PluginSidebarItemHostProps & { theme: PluginTheme }) { + const pathname = usePathname(); + const routeParams = useGlobalSearchParams(); + const { plugin, item } = selectPluginSidebarTarget(group, hostIdFromPathname(pathname)); + const client = useHostRuntimeClient(plugin.serverId); + const toast = useToast(); + // The content outlives `open` so the surface can play its exit and native sheet teardown. + const [popover, setPopover] = useState(null); + const [popoverOpen, setPopoverOpen] = useState(false); + const environment = useMemo( + () => + client + ? { + installation: plugin, + toast, + state: createPluginClientStateSource(plugin.serverId), + } + : null, + [client, plugin, toast], + ); + const showPopover = useCallback((content: PopoverContent | null) => { + // A function passed to a state setter is an updater, so wrap the component. + if (content) setPopover(() => content); + setPopoverOpen(content !== null); + }, []); + const handleOpenChange = useCallback((open: boolean) => { + if (!open) setPopoverOpen(false); + }, []); + if (!environment) return null; + return ( + + + + + + + + ); +} + +const ThemedPluginSidebarItem = withUnistyles(PluginSidebarItemHost); + +function SidebarItemContent({ + plugin, + item, + section, + theme, + currentScreen, + environment, + popover, + showPopover, + fallbackAnchorRef, + onBeforeNavigate, +}: { + plugin: PluginSidebarTarget["plugin"]; + item: PluginSidebarTarget["item"]; + section: PluginSidebarSection; + theme: PluginTheme; + currentScreen: PluginScreenLocation | null; + environment: PluginEnvironment; + popover: PopoverContent | null; + showPopover: (content: PopoverContent | null) => void; + fallbackAnchorRef: RefObject; + onBeforeNavigate?: () => void; +}) { + const { anchorTo, offerAnchor, releaseAnchor, anchorToFallback } = + useSidebarPopoverAnchor("PluginSidebarItem"); + const hosts = useHosts(); + const compact = useIsCompactFormFactor(); + const hostLabel = + hosts.find((host) => host.serverId === plugin.serverId)?.label ?? plugin.serverId; + + const hostProps = useMemo( + () => ({ + theme, + host: { id: plugin.serverId, label: hostLabel }, + layout: { compact, platform: resolvePluginPlatform() }, + }), + [compact, hostLabel, plugin.serverId, theme], + ); + const openScreen = useCallback( + (input: PluginOpenScreenInput) => { + const { screenId, params } = parsePluginOpenScreenInput(plugin, input); + rememberPluginContributionHost(pluginScreensHostKey(plugin.id), plugin.serverId); + showPopover(null); + onBeforeNavigate?.(); + router.push( + buildPluginSurfaceRoute( + plugin.serverId, + plugin.id, + { kind: "surface", id: screenId }, + params, + ), + ); + }, + [onBeforeNavigate, plugin, showPopover], + ); + const openPopover = useCallback( + (Content: PopoverContent) => { + anchorToFallback(fallbackAnchorRef); + showPopover(Content); + }, + [anchorToFallback, fallbackAnchorRef, showPopover], + ); + const close = useCallback(() => showPopover(null), [showPopover]); + const frame = useMemo( + () => ({ + section, + title: item.title, + testID: + section === "header" + ? `plugin-sidebar-${plugin.id}-${item.id}` + : `plugin-sidebar-${section}-${plugin.id}-${item.id}`, + anchorTo, + offerAnchor, + releaseAnchor, + }), + [anchorTo, item.id, item.title, offerAnchor, plugin.id, releaseAnchor, section], + ); + const itemProps = useMemo( + () => ({ ...hostProps, currentScreen, openScreen, openPopover }), + [currentScreen, hostProps, openPopover, openScreen], + ); + const Item = item.Component; + const Popover = popover; + return ( + + + {Popover ? ( + + + + + + + + + + ) : null} + + ); +} diff --git a/packages/app/src/plugins/sidebar-items/kit.tsx b/packages/app/src/plugins/sidebar-items/kit.tsx new file mode 100644 index 0000000000..5796d572ca --- /dev/null +++ b/packages/app/src/plugins/sidebar-items/kit.tsx @@ -0,0 +1,72 @@ +import type { SidebarIcon, SidebarRowProps } from "@getpaseo/plugin/client/ui"; +import { useCallback, useEffect, useRef } from "react"; +import { View } from "react-native"; +import { StyleSheet } from "react-native-unistyles"; +import { SidebarHeaderRow } from "@/components/sidebar/sidebar-header-row"; +import { SidebarSeparator as AppSidebarSeparator } from "@/components/sidebar/sidebar-separator"; +import { resolvePluginIcon } from "../icons"; +import { useSidebarItemFrame } from "./frame"; + +function resolveIcon(icon: SidebarIcon) { + return typeof icon === "string" ? resolvePluginIcon(icon) : icon; +} + +/** + * An item may render several rows. Any press inside a row, on the row or on its trailing content, + * makes that row the popover anchor: the capture-phase responder check runs before the pressed + * child claims the touch (native and web), and returns false so the child still handles it. + */ +export function SidebarRow({ id, icon, label, onPress, active, trailing }: SidebarRowProps) { + const frame = useSidebarItemFrame("SidebarRow"); + const { anchorTo, offerAnchor, releaseAnchor } = frame; + const rowRef = useRef(null); + useEffect(() => { + const node = rowRef.current; + offerAnchor(node); + return () => releaseAnchor(node); + }, [offerAnchor, releaseAnchor]); + const anchorHere = useCallback(() => { + anchorTo(rowRef.current); + return false; + }, [anchorTo]); + // Keyboard activation skips the responder system, so the row's own press anchors as well. + const handlePress = useCallback(() => { + anchorTo(rowRef.current); + onPress(); + }, [anchorTo, onPress]); + return ( + + + + ); +} + +/** The sidebar's separator line, run edge to edge across the section like the app's own. */ +export function SidebarSeparator() { + const frame = useSidebarItemFrame("SidebarSeparator"); + return ( + + + + ); +} + +const styles = StyleSheet.create((theme) => ({ + // Header rows fill the sidebar width; the gap between them is 2px. + header: { + marginVertical: theme.spacing[1], + }, + // The footer pads its rows by `spacing[2]` and spaces them `spacing[1]` apart. + footer: { + marginHorizontal: -theme.spacing[2], + marginVertical: theme.spacing[0.5], + }, +})); diff --git a/packages/app/src/plugins/sidebar-items/legacy.tsx b/packages/app/src/plugins/sidebar-items/legacy.tsx new file mode 100644 index 0000000000..94d3e69029 --- /dev/null +++ b/packages/app/src/plugins/sidebar-items/legacy.tsx @@ -0,0 +1,44 @@ +import { router, usePathname } from "expo-router"; +import { useCallback } from "react"; +import { SidebarHeaderRow } from "@/components/sidebar/sidebar-header-row"; +import { rememberPluginContributionHost } from "../contribution-host"; +import { resolvePluginIcon } from "../icons"; +import { buildPluginSurfaceRoute, hostIdFromPathname } from "../routes"; +import { selectPluginSidebarTarget, type PluginSidebarGroup } from "../sidebar-groups"; + +// COMPAT(pluginSidebarAliases): added in v0.11.0, remove after 2027-03-29 +/** + * An `addSidebarItem({ id, title, icon, surface })` row. It opens `/sidebar/`, so the screen's + * header and host switcher belong to this item. + */ +export function LegacyPluginSidebarRow({ + group, + onBeforeNavigate, +}: { + group: Extract; + onBeforeNavigate?: () => void; +}) { + const pathname = usePathname(); + const identity = { kind: "sidebar", id: group.contributionId } as const; + const target = selectPluginSidebarTarget(group, hostIdFromPathname(pathname)); + const route = buildPluginSurfaceRoute(target.plugin.serverId, group.pluginId, identity); + const isActive = group.targets.some( + (candidate) => + pathname === buildPluginSurfaceRoute(candidate.plugin.serverId, group.pluginId, identity), + ); + const navigate = useCallback(() => { + rememberPluginContributionHost(group.key, target.plugin.serverId); + onBeforeNavigate?.(); + router.push(route); + }, [group.key, onBeforeNavigate, route, target.plugin.serverId]); + return ( + + ); +} diff --git a/packages/app/src/plugins/surface-contribution.test.ts b/packages/app/src/plugins/surface-contribution.test.ts index e6b2eea757..90672ed435 100644 --- a/packages/app/src/plugins/surface-contribution.test.ts +++ b/packages/app/src/plugins/surface-contribution.test.ts @@ -2,7 +2,10 @@ import { QueryClient } from "@tanstack/react-query"; import { describe, expect, it } from "vitest"; import type { InstalledPlugin } from "./types"; import { + currentPluginScreen, getPluginSurfaceContributionServerIds, + parsePluginOpenScreenInput, + resolvePluginScreenTitle, resolvePluginSurfaceContribution, } from "./surface-contribution"; @@ -18,10 +21,13 @@ function installation( clientBundle: serverId, lifetime: new AbortController(), queryClient: new QueryClient(), + paseo: {} as InstalledPlugin["paseo"], + invoke: async () => undefined, cleanup: () => undefined, settingsScreens: [], - surfaces: surfaces.map((id) => ({ id, Component: () => null })), - sidebarItems: sidebarItems.map((item) => ({ + surfaces: surfaces.map((id) => ({ id, title: id, Component: () => null })), + sidebarItems: { header: [], footer: [] }, + legacySidebarItems: sidebarItems.map((item) => ({ ...item, title: item.id, icon: "Blocks", @@ -67,6 +73,7 @@ describe("plugin surface contribution identity", () => { expect(getPluginSurfaceContributionServerIds(installations, "review", identity)).toEqual([ "host-v1", "host-v2", + "same-surface", ]); }); @@ -82,4 +89,129 @@ describe("plugin surface contribution identity", () => { "same-surface", ]); }); + + it("opens a direct screen without borrowing a legacy item that points at it", () => { + const resolved = resolvePluginSurfaceContribution(installations[0] ?? null, { + kind: "surface", + id: "surface-v1", + }); + + expect(resolved.sidebarItem).toBeNull(); + expect(resolved.surface?.id).toBe("surface-v1"); + }); + + it("resolves an old sidebar link to the same-id screen once the plugin has migrated", () => { + const resolved = resolvePluginSurfaceContribution(installations[2] ?? null, { + kind: "sidebar", + id: "overview", + }); + + expect(resolved.sidebarItem).toBeNull(); + expect(resolved.surface?.id).toBe("overview"); + expect( + resolvePluginSurfaceContribution(installations[2] ?? null, { kind: "sidebar", id: "missing" }) + .surface, + ).toBeNull(); + }); +}); + +describe("currentPluginScreen", () => { + const plugin = installation( + "local", + ["overview", "details"], + [{ id: "legacy", surface: "details" }], + ); + + it("reads the open screen and its params from direct and legacy routes on the installation's host", () => { + expect( + currentPluginScreen(plugin, "/h/local/plugin/review/surface/overview", { + serverId: "local", + pluginId: "review", + contributionKind: "surface", + contributionId: "overview", + "param.botId": "bot-2", + }), + ).toEqual({ screenId: "overview", params: { botId: "bot-2" } }); + expect(currentPluginScreen(plugin, "/h/local/plugin/review/sidebar/legacy", {})).toEqual({ + screenId: "details", + params: {}, + }); + }); + + it("is null on another host, another plugin, or a missing screen", () => { + expect(currentPluginScreen(plugin, "/h/remote/plugin/review/surface/overview", {})).toBeNull(); + expect(currentPluginScreen(plugin, "/h/local/plugin/other/surface/overview", {})).toBeNull(); + expect(currentPluginScreen(plugin, "/h/local/plugin/review/surface/missing", {})).toBeNull(); + expect(currentPluginScreen(plugin, "/h/local/workspace/abc", {})).toBeNull(); + }); +}); + +describe("parsePluginOpenScreenInput", () => { + const plugin = installation("local", ["bot"]); + + it("accepts a registered screen with or without string params, under any key", () => { + expect(parsePluginOpenScreenInput(plugin, { screenId: "bot" })).toEqual({ + screenId: "bot", + params: {}, + }); + expect(parsePluginOpenScreenInput(plugin, { screenId: "bot", params: { botId: "2" } })).toEqual( + { screenId: "bot", params: { botId: "2" } }, + ); + expect( + parsePluginOpenScreenInput(plugin, { screenId: "bot", params: { serverId: "x" } }), + ).toEqual({ screenId: "bot", params: { serverId: "x" } }); + }); + + it("throws on a bare id, an unknown screen, or params that are not strings", () => { + expect(() => parsePluginOpenScreenInput(plugin, "bot")).toThrow("{ screenId, params? }"); + expect(() => parsePluginOpenScreenInput(plugin, { screenId: "missing" })).toThrow( + "unavailable: missing", + ); + expect(() => + parsePluginOpenScreenInput(plugin, { screenId: "bot", params: { botId: 2 } }), + ).toThrow("botId must be a string"); + expect(() => parsePluginOpenScreenInput(plugin, { screenId: "bot", params: ["2"] })).toThrow( + "must be an object of strings", + ); + }); +}); + +describe("resolvePluginScreenTitle", () => { + const Component = () => null; + const botNames: Record = { "bot-1": "Bot 1", "bot-2": "Bot 2" }; + const bot = { + id: "bot", + title: (params: Record) => botNames[params.botId] ?? "", + Component, + }; + + it("uses a string title as is", () => { + expect(resolvePluginScreenTitle({ id: "deploys", title: "Deploys", Component }, null, {})).toBe( + "Deploys", + ); + }); + + it("derives a function title from the params it is given", () => { + expect(resolvePluginScreenTitle(bot, null, { botId: "bot-2" })).toBe("Bot 2"); + expect(resolvePluginScreenTitle(bot, null, { botId: "bot-1" })).toBe("Bot 1"); + }); + + it("falls back to the screen id when the function returns empty or throws", () => { + expect(resolvePluginScreenTitle(bot, null, { botId: "missing" })).toBe("bot"); + const throwing = { + id: "bot", + title: () => { + throw new Error("no bots loaded"); + }, + Component, + }; + expect(resolvePluginScreenTitle(throwing, null, {})).toBe("bot"); + }); + + it("uses the title of a legacy item that points at the screen", () => { + const legacy = { id: "entry", title: "Legacy entry", icon: "Server", surface: "main" }; + expect(resolvePluginScreenTitle({ id: "main", title: "main", Component }, legacy, {})).toBe( + "Legacy entry", + ); + }); }); diff --git a/packages/app/src/plugins/surface-contribution.ts b/packages/app/src/plugins/surface-contribution.ts index 743ebbc1a7..830677f164 100644 --- a/packages/app/src/plugins/surface-contribution.ts +++ b/packages/app/src/plugins/surface-contribution.ts @@ -1,39 +1,100 @@ +import type { PluginScreenLocation, PluginScreenParams } from "@getpaseo/plugin/client"; +import { + parsePluginScreenParams, + parsePluginSurfaceRoute, + pluginScreenParamsFromRoute, +} from "./routes"; import type { InstalledPlugin } from "./types"; export type PluginSurfaceContributionIdentity = | { kind: "sidebar"; id: string } | { kind: "surface"; id: string }; +/** + * A `sidebar` identity is a legacy `addSidebarItem` route: it resolves to that item and its screen, + * or, once the plugin has moved that item to `addScreen` under the same id, to that screen. + */ export function resolvePluginSurfaceContribution( plugin: InstalledPlugin | null, identity: PluginSurfaceContributionIdentity | null, ): { - sidebarItem: InstalledPlugin["sidebarItems"][number] | null; + sidebarItem: InstalledPlugin["legacySidebarItems"][number] | null; surface: InstalledPlugin["surfaces"][number] | null; } { - if (!identity) return { sidebarItem: null, surface: null }; + if (!identity || !plugin) return { sidebarItem: null, surface: null }; const sidebarItem = identity.kind === "sidebar" - ? (plugin?.sidebarItems.find((contribution) => contribution.id === identity.id) ?? null) + ? (plugin.legacySidebarItems.find((contribution) => contribution.id === identity.id) ?? null) : null; - const surfaceId = identity.kind === "sidebar" ? sidebarItem?.surface : identity.id; + const surfaceId = sidebarItem ? sidebarItem.surface : identity.id; const surface = surfaceId - ? (plugin?.surfaces.find((contribution) => contribution.id === surfaceId) ?? null) + ? (plugin.surfaces.find((contribution) => contribution.id === surfaceId) ?? null) : null; return { sidebarItem, surface }; } +/** + * The screen header's title. A legacy `addSidebarItem` pointing at the screen lends its title, as + * before screens had one. A title function that throws or returns an empty string falls back to the + * screen id, so a bad title never takes the screen down. + */ +export function resolvePluginScreenTitle( + screen: InstalledPlugin["surfaces"][number], + legacySidebarItem: InstalledPlugin["legacySidebarItems"][number] | null, + params: PluginScreenParams, +): string { + if (legacySidebarItem) return legacySidebarItem.title; + if (typeof screen.title === "string") return screen.title; + try { + const title = screen.title(params); + if (typeof title === "string" && title.trim()) return title.trim(); + } catch (error) { + console.warn(`[Plugins] Screen ${screen.id} title failed`, error); + } + return screen.id; +} + +/** + * The installation's screen open at `pathname` with the route's search params, or null when the + * route shows anything else. + */ +export function currentPluginScreen( + plugin: InstalledPlugin, + pathname: string, + routeParams: Readonly>, +): PluginScreenLocation | null { + const route = parsePluginSurfaceRoute(pathname); + if (!route || route.serverId !== plugin.serverId || route.pluginId !== plugin.id) return null; + const screenId = resolvePluginSurfaceContribution(plugin, route.identity).surface?.id; + return screenId ? { screenId, params: pluginScreenParamsFromRoute(routeParams) } : null; +} + +/** Checks an `openScreen` input against the installation's screens. Throws when it is invalid. */ +export function parsePluginOpenScreenInput( + plugin: InstalledPlugin, + input: unknown, +): PluginScreenLocation { + if (typeof input !== "object" || input === null || !("screenId" in input)) { + throw new Error("openScreen takes { screenId, params? }"); + } + const { screenId } = input; + if (typeof screenId !== "string" || !plugin.surfaces.some((surface) => surface.id === screenId)) { + throw new Error(`Plugin screen is unavailable: ${String(screenId)}`); + } + const params = "params" in input ? input.params : undefined; + return { screenId, params: parsePluginScreenParams(params) }; +} + export function getPluginSurfaceContributionServerIds( installations: readonly InstalledPlugin[], pluginId: string, identity: PluginSurfaceContributionIdentity, ): string[] { return installations - .filter((installation) => { - if (installation.id !== pluginId) return false; - return identity.kind === "sidebar" - ? installation.sidebarItems.some((contribution) => contribution.id === identity.id) - : installation.surfaces.some((contribution) => contribution.id === identity.id); - }) + .filter( + (installation) => + installation.id === pluginId && + resolvePluginSurfaceContribution(installation, identity).surface !== null, + ) .map((installation) => installation.serverId); } diff --git a/packages/app/src/plugins/surface-runtime.test.tsx b/packages/app/src/plugins/surface-runtime.test.tsx deleted file mode 100644 index 30b2a7f76b..0000000000 --- a/packages/app/src/plugins/surface-runtime.test.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import type { DaemonClient } from "@getpaseo/client/internal/daemon-client"; -import type { PaseoApi } from "@getpaseo/client"; -import { PaseoApiProvider } from "@getpaseo/plugin/client/host"; -import { usePaseo } from "@getpaseo/plugin/client"; -import React from "react"; -import { renderToStaticMarkup } from "react-dom/server"; -import { describe, expect, it, vi } from "vitest"; -import { createPluginSurfaceRuntime } from "./surface-runtime"; - -function clientWithWorkspace(id: string) { - const createWorkspace = vi.fn(async () => ({ - error: null, - workspace: { - id, - projectId: `project-${id}`, - workspaceDirectory: `/tmp/${id}`, - name: id, - status: "active", - }, - })); - const createAgent = vi.fn(async () => ({ - id: `agent-${id}`, - provider: "codex", - cwd: `/tmp/${id}`, - workspaceId: id, - status: "idle", - })); - const invokePluginRpc = vi.fn(async () => id); - return { - client: { createWorkspace, createAgent, invokePluginRpc } as unknown as DaemonClient, - createWorkspace, - createAgent, - invokePluginRpc, - }; -} - -function borrowFromAppProvider(paseo: PaseoApi): PaseoApi { - let borrowed: PaseoApi | null = null; - function PluginSurface() { - borrowed = usePaseo(); - return null; - } - renderToStaticMarkup( - - - , - ); - if (!borrowed) throw new Error("Plugin surface did not receive Paseo API"); - return borrowed; -} - -describe("plugin surface host runtime", () => { - it("creates a PR worktree and agent through usePaseo on the selected app host", async () => { - const selected = clientWithWorkspace("workspace-a"); - const runtime = createPluginSurfaceRuntime(selected.client, { - id: "workspace-plugin", - lifetime: new AbortController(), - }); - if (!runtime) throw new Error("Expected selected host runtime"); - - const paseo = borrowFromAppProvider(runtime.paseo); - const workspace = await paseo.workspaces.create({ - source: { - kind: "worktree", - cwd: "/tmp/repository", - action: "checkout", - checkoutSource: { kind: "change_request", forge: "github", number: 42 }, - }, - }); - const agent = await workspace.agents.create({ - config: { provider: "codex/gpt-5" }, - prompt: "Review PR #42", - }); - - expect(workspace.id).toBe("workspace-a"); - expect(agent.id).toBe("agent-workspace-a"); - expect(selected.createWorkspace).toHaveBeenCalledOnce(); - expect(selected.createAgent).toHaveBeenCalledOnce(); - }); - - it("switches all plugin calls when the selected host changes", async () => { - const hostA = clientWithWorkspace("workspace-a"); - const hostB = clientWithWorkspace("workspace-b"); - const first = createPluginSurfaceRuntime(hostA.client, { - id: "same-plugin", - lifetime: new AbortController(), - }); - const second = createPluginSurfaceRuntime(hostB.client, { - id: "same-plugin", - lifetime: new AbortController(), - }); - if (!first || !second) throw new Error("Expected online host runtimes"); - - await first.invoke("host", {}); - await borrowFromAppProvider(second.paseo).workspaces.create({ - source: { kind: "directory", path: "/tmp/workspace-b" }, - }); - - expect(hostA.invokePluginRpc).toHaveBeenCalledWith("same-plugin", "host", {}); - expect(hostA.createWorkspace).not.toHaveBeenCalled(); - expect(hostB.createWorkspace).toHaveBeenCalledOnce(); - }); - - it("keeps an offline selected host unavailable instead of borrowing another host", () => { - const otherHost = clientWithWorkspace("workspace-online"); - - expect( - createPluginSurfaceRuntime(null, { id: "same-plugin", lifetime: new AbortController() }), - ).toBeNull(); - expect(otherHost.createWorkspace).not.toHaveBeenCalled(); - }); -}); diff --git a/packages/app/src/plugins/surface-runtime.ts b/packages/app/src/plugins/surface-runtime.ts deleted file mode 100644 index 3c99e38aba..0000000000 --- a/packages/app/src/plugins/surface-runtime.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { useEffect, useState } from "react"; -import type { InstalledPlugin } from "./types"; -import { createPaseoApi, type PaseoApi } from "@getpaseo/client"; -import type { DaemonClient } from "@getpaseo/client/internal/daemon-client"; - -export interface PluginSurfaceRuntime { - paseo: PaseoApi; - invoke(method: string, input: unknown): Promise; -} - -export function createPluginSurfaceRuntime( - client: DaemonClient | null, - plugin: Pick, -): PluginSurfaceRuntime | null { - if (!client || plugin.lifetime.signal.aborted) return null; - return { - paseo: createPaseoApi(client, { signal: plugin.lifetime.signal }), - invoke: (method, input) => client.invokePluginRpc(plugin.id, method, input), - }; -} - -/** A mounted surface owns its API; creating a React element creates no server demand. */ -export function usePluginSurfaceRuntime( - client: DaemonClient | null, - plugin: InstalledPlugin | null | undefined, -): PluginSurfaceRuntime | null { - const [mounted, setMounted] = useState<{ - client: DaemonClient; - plugin: InstalledPlugin; - runtime: PluginSurfaceRuntime; - } | null>(null); - useEffect(() => { - if (!client || !plugin) return; - const runtime = createPluginSurfaceRuntime(client, plugin); - if (!runtime) return; - setMounted({ client, plugin, runtime }); - return () => { - void runtime.paseo - .dispose() - .catch((error) => console.warn(`[Plugins] Surface cleanup failed for ${plugin.id}`, error)); - }; - }, [client, plugin]); - return mounted?.client === client && mounted.plugin === plugin ? mounted.runtime : null; -} diff --git a/packages/app/src/plugins/surface-screen.tsx b/packages/app/src/plugins/surface-screen.tsx index c827cf158b..c6e7b0a6cc 100644 --- a/packages/app/src/plugins/surface-screen.tsx +++ b/packages/app/src/plugins/surface-screen.tsx @@ -1,15 +1,15 @@ import { router, useLocalSearchParams } from "expo-router"; -import type { PluginSurfaceProps } from "@getpaseo/plugin/client"; +import type { PluginScreenParams, PluginScreenProps } from "@getpaseo/plugin/client"; import type { PluginTheme } from "@getpaseo/plugin"; -import { ChevronDown, X } from "lucide-react-native"; -import { useCallback, useMemo, useRef, useState, type ComponentType } from "react"; -import { Platform, Pressable, Text, View } from "react-native"; +import { X } from "lucide-react-native"; +import { useCallback, useMemo, type ComponentType } from "react"; +import { Text, View } from "react-native"; import { StyleSheet, withUnistyles } from "react-native-unistyles"; import { HeaderIconBadge } from "@/components/headers/header-icon-badge"; import { HeaderToggleButton } from "@/components/headers/header-toggle-button"; import { ScreenHeader } from "@/components/headers/screen-header"; import { ScreenTitle } from "@/components/headers/screen-title"; -import { HostPicker } from "@/components/hosts/host-picker"; +import { HostSwitcher } from "@/components/hosts/host-switcher"; import { useIsCompactFormFactor } from "@/constants/layout"; import { useHostRuntimeClient, useHosts } from "@/runtime/host-runtime"; import type { Theme } from "@/styles/theme"; @@ -18,16 +18,21 @@ import { usePluginHostNavigation } from "./host-navigation"; import { resolvePluginIcon } from "./icons"; import { toPluginTheme } from "./theme"; import { useInstalledPlugin, usePluginInstallations } from "./registry"; -import { buildPluginSurfaceRoute } from "./routes"; -import { rememberPluginContributionHost } from "./contribution-host"; +import { buildPluginSurfaceRoute, pluginScreenParamsFromRoute } from "./routes"; +import { + legacySidebarItemHostKey, + pluginScreensHostKey, + rememberPluginContributionHost, +} from "./contribution-host"; import { SurfaceErrorBoundary } from "./surface-error-boundary"; -import type { DaemonClient } from "@getpaseo/client/internal/daemon-client"; -import { PluginRuntimeBoundary } from "./runtime-boundary"; +import { PluginInstallationProvider } from "./installation-provider"; import { getPluginSurfaceContributionServerIds, + resolvePluginScreenTitle, resolvePluginSurfaceContribution, type PluginSurfaceContributionIdentity, } from "./surface-contribution"; +import { resolvePluginPlatform } from "./platform"; const EMPTY_SHORTCUT_KEYS: ShortcutKey[] = []; const mutedColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted }); @@ -35,7 +40,6 @@ const pluginThemeMapping = (theme: Theme) => ({ theme: toPluginTheme(theme), }); const ThemedX = withUnistyles(X); -const ThemedChevronDown = withUnistyles(ChevronDown); function routeParam(value: string | string[] | undefined): string { return typeof value === "string" ? value : ""; @@ -55,44 +59,40 @@ const ThemedPluginHeaderIcon = withUnistyles(PluginHeaderIcon); function SurfaceRenderer({ Surface, - client, plugin, layout, host, + params, theme, }: { - Surface: ComponentType; - client: DaemonClient; + Surface: ComponentType; plugin: NonNullable>; - layout: PluginSurfaceProps["layout"]; - host: PluginSurfaceProps["host"]; + layout: PluginScreenProps["layout"]; + host: PluginScreenProps["host"]; + params: PluginScreenParams; theme: PluginTheme; }) { const navigation = usePluginHostNavigation(host.id); return ( - - - + + + ); } const ThemedSurfaceRenderer = withUnistyles(SurfaceRenderer); -function resolvePlatform(): PluginSurfaceProps["layout"]["platform"] { - if (Platform.OS === "ios") return "ios"; - if (Platform.OS === "android") return "android"; - return "web"; -} - function PluginHostSwitcher({ serverId, pluginId, identity, + params, serverIds, }: { serverId: string; pluginId: string; identity: PluginSurfaceContributionIdentity; + params: PluginScreenParams; serverIds: string[]; }) { const allHosts = useHosts(); @@ -100,60 +100,48 @@ function PluginHostSwitcher({ () => allHosts.filter((host) => serverIds.includes(host.serverId)), [allHosts, serverIds], ); - const [open, setOpen] = useState(false); - const anchorRef = useRef(null); - const selectedLabel = hosts.find((host) => host.serverId === serverId)?.label ?? serverId; const selectHost = useCallback( (nextServerId: string) => { - rememberPluginContributionHost(`${pluginId}/${identity.kind}/${identity.id}`, nextServerId); - router.replace(buildPluginSurfaceRoute(nextServerId, pluginId, identity)); + // A legacy row remembers its own host; a screen's host carries to all the plugin's items. + rememberPluginContributionHost( + identity.kind === "sidebar" + ? legacySidebarItemHostKey(pluginId, identity.id) + : pluginScreensHostKey(pluginId), + nextServerId, + ); + router.replace(buildPluginSurfaceRoute(nextServerId, pluginId, identity, params)); }, - [identity, pluginId], + [identity, params, pluginId], ); - const openPicker = useCallback(() => setOpen(true), []); const show = serverIds.length > 1 && hosts.length > 1; if (!show) return null; return ( - - - - - {selectedLabel} - - - - - + accessibilityLabel="Plugin host" + testID="plugin-host-switcher" + /> ); } export function PluginSurfaceScreen() { - const params = useLocalSearchParams<{ + const routeParams = useLocalSearchParams<{ serverId?: string | string[]; pluginId?: string | string[]; contributionKind?: string | string[]; contributionId?: string | string[]; }>(); - const serverId = routeParam(params.serverId); - const pluginId = routeParam(params.pluginId); - const contributionKind = routeParam(params.contributionKind); - const contributionId = routeParam(params.contributionId); + const serverId = routeParam(routeParams.serverId); + const pluginId = routeParam(routeParams.pluginId); + const contributionKind = routeParam(routeParams.contributionKind); + const contributionId = routeParam(routeParams.contributionId); + const paramsKey = JSON.stringify(pluginScreenParamsFromRoute(routeParams)); + // Keyed by content: the route hands back a new object on every render. + const params = useMemo(() => JSON.parse(paramsKey), [paramsKey]); const identity = useMemo(() => { if (contributionKind !== "sidebar" && contributionKind !== "surface") return null; return { kind: contributionKind, id: contributionId }; @@ -173,13 +161,19 @@ export function PluginSurfaceScreen() { identity ? getPluginSurfaceContributionServerIds(installations, pluginId, identity) : [], [identity, installations, pluginId], ); - const title = sidebarItem?.title ?? surface?.id ?? (pluginId || "Plugin"); + const title = useMemo( + () => + surface + ? resolvePluginScreenTitle(surface, sidebarItem, params) + : (sidebarItem?.title ?? (pluginId || "Plugin")), + [params, pluginId, sidebarItem, surface], + ); const Icon = sidebarItem ? resolvePluginIcon(sidebarItem.icon) : null; const close = useCallback(() => { if (router.canGoBack()) router.back(); else router.replace(`/h/${encodeURIComponent(serverId)}`); }, [serverId]); - const layout = useMemo(() => ({ compact, platform: resolvePlatform() }), [compact]); + const layout = useMemo(() => ({ compact, platform: resolvePluginPlatform() }), [compact]); const host = useMemo(() => ({ id: serverId, label: hostLabel }), [hostLabel, serverId]); const headerLeft = useMemo( () => ( @@ -189,7 +183,7 @@ export function PluginSurfaceScreen() { ) : null} - {title} + {title} ), [Icon, title], @@ -202,6 +196,7 @@ export function PluginSurfaceScreen() { serverId={serverId} pluginId={pluginId} identity={identity} + params={params} serverIds={contributionServerIds} /> ) : null} @@ -217,7 +212,7 @@ export function PluginSurfaceScreen() { ), - [close, contributionServerIds, identity, pluginId, serverId], + [close, contributionServerIds, identity, params, pluginId, serverId], ); return ( @@ -232,10 +227,10 @@ export function PluginSurfaceScreen() { > @@ -261,19 +256,4 @@ const styles = StyleSheet.create((theme) => ({ color: theme.colors.statusDanger, padding: theme.spacing[4], }, - hostSwitcher: { - maxWidth: 180, - minHeight: 32, - paddingHorizontal: theme.spacing[2], - flexDirection: "row", - alignItems: "center", - gap: theme.spacing[1], - borderRadius: theme.borderRadius.md, - backgroundColor: theme.colors.surface1, - }, - hostSwitcherText: { - flexShrink: 1, - color: theme.colors.foregroundMuted, - fontSize: theme.fontSize.base, - }, })); diff --git a/packages/app/src/plugins/theme.test.ts b/packages/app/src/plugins/theme.test.ts index d7c7537ade..c1548d0521 100644 --- a/packages/app/src/plugins/theme.test.ts +++ b/packages/app/src/plugins/theme.test.ts @@ -51,9 +51,12 @@ function installed(serverId: string, themes: PluginThemeContribution[]): Install clientBundle: serverId, lifetime: new AbortController(), queryClient: new QueryClient(), + paseo: {} as InstalledPlugin["paseo"], + invoke: async () => undefined, settingsScreens: [], surfaces: [], - sidebarItems: [], + sidebarItems: { header: [], footer: [] }, + legacySidebarItems: [], workspacePanels: [], commandCenterItems: [], clientSlashCommands: [], diff --git a/packages/app/src/plugins/timeline/model.test.ts b/packages/app/src/plugins/timeline/model.test.ts index 68e1378199..c64e498333 100644 --- a/packages/app/src/plugins/timeline/model.test.ts +++ b/packages/app/src/plugins/timeline/model.test.ts @@ -13,10 +13,13 @@ function plugin(input: { clientBundle: "bundle", lifetime: new AbortController(), queryClient: new QueryClient(), + paseo: {} as InstalledPlugin["paseo"], + invoke: async () => undefined, cleanup: () => {}, settingsScreens: [], surfaces: [], - sidebarItems: [], + sidebarItems: { header: [], footer: [] }, + legacySidebarItems: [], workspacePanels: [], commandCenterItems: [], clientSlashCommands: [], diff --git a/packages/app/src/plugins/timeline/view.test.tsx b/packages/app/src/plugins/timeline/view.test.tsx index 59d7c3ff36..af1e711eb5 100644 --- a/packages/app/src/plugins/timeline/view.test.tsx +++ b/packages/app/src/plugins/timeline/view.test.tsx @@ -13,7 +13,8 @@ vi.mock("@/runtime/host-runtime", () => ({ useHostRuntimeClient: () => selectedHost.client ?? (hostClient as unknown as DaemonClient), useHosts: () => [{ serverId: "host-1", label: "Local" }], })); -vi.mock("@/constants/layout", () => ({ +vi.mock("@/constants/layout", async (importOriginal) => ({ + ...(await importOriginal()), useIsCompactFormFactor: () => false, })); vi.mock("../navigation", () => ({ @@ -210,7 +211,7 @@ describe("PluginTimelineItemView", () => { }); }); -it("releases a crashed renderer's observations and recovers a fresh scope in StrictMode", async () => { +it("a renderer releases its observation on the plugin's client when it crashes, and remounts in StrictMode", async () => { let receive: Parameters[0] = () => {}; let opened: () => void = () => {}; let nextId = 0; @@ -285,11 +286,9 @@ it("releases a crashed renderer's observations and recovers a fresh scope in Str return { default(plugin) { function Card(props) { const paseo = usePaseo(); - const failed = React.useRef(false); - failed.current = props.item.data.label === "explode"; React.useEffect(() => { const owner = paseo.observeEvents(["project.update"]); - return () => { if (failed.current) throw new Error("plugin cleanup failed"); void owner.release(); }; + return () => { void owner.release(); }; }, [paseo]); if (props.item.data.label === "explode") throw new Error("owned renderer failed"); return React.createElement("span", null, props.item.data.label); diff --git a/packages/app/src/plugins/timeline/view.tsx b/packages/app/src/plugins/timeline/view.tsx index 1490d8e14e..90f6e0e1a2 100644 --- a/packages/app/src/plugins/timeline/view.tsx +++ b/packages/app/src/plugins/timeline/view.tsx @@ -1,8 +1,8 @@ import { PluginClientStateProvider } from "@getpaseo/plugin/client/host"; -import type { PluginHostProps, PluginTimelineItemProps } from "@getpaseo/plugin/client"; +import type { PluginTimelineItemProps } from "@getpaseo/plugin/client"; import type { PluginTheme } from "@getpaseo/plugin"; import React, { type ComponentType, useMemo } from "react"; -import { Platform, Text } from "react-native"; +import { Text } from "react-native"; import { StyleSheet, withUnistyles } from "react-native-unistyles"; import { useIsCompactFormFactor } from "@/constants/layout"; import { useHostRuntimeClient, useHosts } from "@/runtime/host-runtime"; @@ -10,18 +10,13 @@ import type { Theme } from "@/styles/theme"; import type { PluginTimelineStreamItem } from "@/types/stream"; import { createPluginClientStateSource } from "../client-state/source"; import { useInstalledPlugin } from "../registry"; -import { PluginRuntimeBoundary } from "../runtime-boundary"; +import { PluginInstallationProvider } from "../installation-provider"; import { SurfaceErrorBoundary } from "../surface-error-boundary"; import { toPluginTheme } from "../theme"; +import { resolvePluginPlatform } from "../platform"; const pluginThemeMapping = (theme: Theme) => ({ theme: toPluginTheme(theme) }); -function resolvePlatform(): PluginHostProps["layout"]["platform"] { - if (Platform.OS === "ios") return "ios"; - if (Platform.OS === "android") return "android"; - return "web"; -} - function TimelineItemUnavailable() { return Plugin timeline item unavailable.; } @@ -63,7 +58,7 @@ function PluginTimelineItemBody({ const hosts = useHosts(); const hostLabel = hosts.find((host) => host.serverId === serverId)?.label ?? serverId; const host = useMemo(() => ({ id: serverId, label: hostLabel }), [hostLabel, serverId]); - const layout = useMemo(() => ({ compact, platform: resolvePlatform() }), [compact]); + const layout = useMemo(() => ({ compact, platform: resolvePluginPlatform() }), [compact]); const stateSource = useMemo(() => createPluginClientStateSource(serverId), [serverId]); if (!plugin || !renderer || !parsed || !client) { @@ -86,11 +81,11 @@ function PluginTimelineItemBody({ }; return ( - + - + ); } diff --git a/packages/app/src/plugins/types.ts b/packages/app/src/plugins/types.ts index 9305cc3ba8..bab295d623 100644 --- a/packages/app/src/plugins/types.ts +++ b/packages/app/src/plugins/types.ts @@ -1,3 +1,4 @@ +import type { PaseoApi } from "@getpaseo/client"; import type { QueryClient } from "@tanstack/react-query"; import type { PluginRequirements } from "@getpaseo/protocol/messages"; import type { @@ -10,7 +11,8 @@ import type { PluginClientSlashCommandContribution, PluginComposerPillContribution, PluginSidebarContribution, - PluginSurfaceContribution, + PluginSidebarItemContribution, + PluginScreenContribution, PluginSettingsScreenContribution, PluginTimelineRendererContribution, PluginTimelineTransformerContribution, @@ -18,6 +20,8 @@ import type { PluginWorkspacePanelContribution, } from "@getpaseo/plugin/client"; +export type PluginSidebarSection = "header" | "footer"; + export type EvaluatedPluginWorkspacePanelContribution = PluginWorkspacePanelContribution & { locations: readonly PluginPanelLocation[]; }; @@ -25,9 +29,12 @@ export type EvaluatedPluginWorkspacePanelContribution = PluginWorkspacePanelCont export interface EvaluatedPlugin { id: string; cleanup: PluginCleanup; - surfaces: PluginSurfaceContribution[]; + surfaces: PluginScreenContribution[]; settingsScreens: PluginSettingsScreenContribution[]; - sidebarItems: PluginSidebarContribution[]; + sidebarItems: Record; + // COMPAT(pluginSidebarAliases): added in v0.11.0, remove after 2027-03-29 + /** `addSidebarItem` registrations, so `/plugin//sidebar/` routes keep resolving. */ + legacySidebarItems: PluginSidebarContribution[]; workspacePanels: EvaluatedPluginWorkspacePanelContribution[]; commandCenterItems: PluginCommandCenterItemContribution[]; clientSlashCommands: PluginClientSlashCommandContribution[]; @@ -39,6 +46,10 @@ export interface EvaluatedPlugin { export interface InstalledPlugin extends EvaluatedPlugin { lifetime: AbortController; + /** The plugin's one Paseo client, `usePaseo()` in every surface; disposed at teardown. */ + paseo: PaseoApi; + /** Calls one of the plugin's server RPC methods on its host. */ + invoke(method: string, input: unknown): Promise; serverId: string; requirements?: PluginRequirements; clientBundle: string; @@ -51,7 +62,8 @@ export type { PluginClientSlashCommandContribution, PluginComposerPillContribution, PluginSidebarContribution, - PluginSurfaceContribution, + PluginSidebarItemContribution, + PluginScreenContribution, PluginSettingsScreenContribution, PluginThemeContribution, PluginTimelineRendererContribution, diff --git a/packages/app/src/plugins/workspace-panels/panel.tsx b/packages/app/src/plugins/workspace-panels/panel.tsx index 78a8bb462a..998f7e90ff 100644 --- a/packages/app/src/plugins/workspace-panels/panel.tsx +++ b/packages/app/src/plugins/workspace-panels/panel.tsx @@ -1,13 +1,9 @@ -import type { - PluginAgentPanelProps, - PluginHostProps, - PluginWorkspacePanelProps, -} from "@getpaseo/plugin/client"; +import type { PluginAgentPanelProps, PluginWorkspacePanelProps } from "@getpaseo/plugin/client"; import type { PluginTheme } from "@getpaseo/plugin"; import { PluginClientStateProvider } from "@getpaseo/plugin/client/host"; import { CircleAlert } from "lucide-react-native"; import { useMemo } from "react"; -import { Platform, Text, View } from "react-native"; +import { Text, View } from "react-native"; import { StyleSheet, withUnistyles } from "react-native-unistyles"; import invariant from "tiny-invariant"; import { useIsCompactFormFactor } from "@/constants/layout"; @@ -23,20 +19,15 @@ import { createPluginClientStateSource } from "../client-state/source"; import { toPluginTheme } from "../theme"; import { resolvePluginIcon } from "../icons"; import { useInstalledPlugin } from "../registry"; -import { PluginRuntimeBoundary } from "../runtime-boundary"; +import { PluginInstallationProvider } from "../installation-provider"; import { SurfaceErrorBoundary } from "../surface-error-boundary"; import { resolvePluginWorkspacePanel } from "./resolution"; +import { resolvePluginPlatform } from "../platform"; const pluginThemeMapping = (theme: Theme) => ({ theme: toPluginTheme(theme), }); -function resolvePlatform(): PluginHostProps["layout"]["platform"] { - if (Platform.OS === "ios") return "ios"; - if (Platform.OS === "android") return "android"; - return "web"; -} - function PluginPanelBody({ theme }: { theme: PluginTheme }) { const { serverId, workspaceId, target } = usePaneContext(); invariant(target.kind === "plugin", "PluginPanel requires plugin target"); @@ -57,7 +48,7 @@ function PluginPanelBody({ theme }: { theme: PluginTheme }) { const hosts = useHosts(); const hostLabel = hosts.find((host) => host.serverId === serverId)?.label ?? serverId; const host = useMemo(() => ({ id: serverId, label: hostLabel }), [hostLabel, serverId]); - const layout = useMemo(() => ({ compact, platform: resolvePlatform() }), [compact]); + const layout = useMemo(() => ({ compact, platform: resolvePluginPlatform() }), [compact]); const stateSource = useMemo(() => createPluginClientStateSource(serverId), [serverId]); const navigation = usePluginHostNavigation(serverId); @@ -105,9 +96,9 @@ function PluginPanelBody({ theme }: { theme: PluginTheme }) { Surface={Surface} key={`${serverId}/${target.pluginId}/${target.panelId}/${target.context}`} > - + {panel} - + ); } diff --git a/packages/app/src/plugins/workspace-panels/resolution.test.ts b/packages/app/src/plugins/workspace-panels/resolution.test.ts index c4ad2ef302..a7e46de422 100644 --- a/packages/app/src/plugins/workspace-panels/resolution.test.ts +++ b/packages/app/src/plugins/workspace-panels/resolution.test.ts @@ -10,10 +10,13 @@ function installed(): InstalledPlugin { clientBundle: "bundle", lifetime: new AbortController(), queryClient: new QueryClient(), + paseo: {} as InstalledPlugin["paseo"], + invoke: async () => undefined, cleanup: () => {}, settingsScreens: [], surfaces: [], - sidebarItems: [], + sidebarItems: { header: [], footer: [] }, + legacySidebarItems: [], workspacePanels: [ { id: "details", diff --git a/packages/app/src/screens/settings/sidebar/sidebar-nav-section.tsx b/packages/app/src/screens/settings/sidebar/sidebar-nav-section.tsx index 90d6e02344..e6785e6784 100644 --- a/packages/app/src/screens/settings/sidebar/sidebar-nav-section.tsx +++ b/packages/app/src/screens/settings/sidebar/sidebar-nav-section.tsx @@ -5,7 +5,9 @@ import type { TFunction } from "i18next"; import { ArrowDown, ArrowUp, + Blocks, CalendarClock, + Gauge, History, Plus, Search, @@ -16,14 +18,15 @@ import { Button } from "@/components/ui/button"; import { Shortcut } from "@/components/ui/shortcut"; import { Switch } from "@/components/ui/switch"; import { useShortcutKeys } from "@/hooks/use-shortcut-keys"; -import { resolvePluginIcon } from "@/plugins/icons"; import { SettingsSection } from "@/components/settings/headings/settings-section"; import { builtinSidebarNavLabelKey, builtinSidebarNavShortcutAction, - type BuiltinSidebarNavId, + type BuiltinSidebarItemId, type SidebarNavItem, + type SidebarSection, } from "@/sidebar-nav/model"; +import { resolvePluginIcon } from "@/plugins/icons"; import { useSidebarNavItems } from "@/sidebar-nav/use-sidebar-nav-items"; import { settingsStyles } from "@/styles/settings"; import { ICON_SIZE, type Theme } from "@/styles/theme"; @@ -36,13 +39,17 @@ const ThemedArrowDown = withUnistyles(ArrowDown); const moveUpIcon = ; const moveDownIcon = ; -const BUILTIN_ICONS: Record = { +const BUILTIN_ICONS: Record = { "new-workspace": Plus, history: History, search: Search, schedules: CalendarClock, + usage: Gauge, }; +/** Plugin items register no icon, so they share this one; a legacy `addSidebarItem` keeps its own. */ +const PLUGIN_ICON = Blocks; + function NavIcon({ Icon, color = "" }: { Icon: LucideIcon; color?: string }) { return ; } @@ -50,7 +57,8 @@ function NavIcon({ Icon, color = "" }: { Icon: LucideIcon; color?: string }) { const ThemedNavIcon = withUnistyles(NavIcon); function navItemIcon(item: SidebarNavItem): LucideIcon { - return item.kind === "builtin" ? BUILTIN_ICONS[item.id] : resolvePluginIcon(item.group.icon); + if (item.kind === "builtin") return BUILTIN_ICONS[item.id]; + return item.group.kind === "legacy" ? resolvePluginIcon(item.group.icon) : PLUGIN_ICON; } function navItemLabel(t: TFunction, item: SidebarNavItem): string { @@ -134,15 +142,26 @@ function SidebarNavRow({ ); } -export function SidebarNavSection(): ReactElement { +const SECTION_COPY = { + header: { + title: "settings.appearance.sidebar.header.title", + info: "settings.appearance.sidebar.header.description", + }, + footer: { + title: "settings.appearance.sidebar.footer.title", + info: "settings.appearance.sidebar.footer.description", + }, +} as const satisfies Record; + +function SidebarItemsCard({ section }: { section: SidebarSection }): ReactElement { const { t } = useTranslation(); - const { items, setVisible, move } = useSidebarNavItems(); + const { items, setVisible, move } = useSidebarNavItems(section); return ( {items.map((item, index) => ( @@ -160,6 +179,16 @@ export function SidebarNavSection(): ReactElement { ); } +/** Settings > Sidebar: one card per section. The footer's Add project row and icon row are fixed and not listed. */ +export function SidebarNavSection(): ReactElement { + return ( + <> + + + + ); +} + const styles = StyleSheet.create((theme) => ({ row: { gap: theme.spacing[2], diff --git a/packages/app/src/sidebar-nav/model.test.ts b/packages/app/src/sidebar-nav/model.test.ts index fa1ef6763c..33604b72e9 100644 --- a/packages/app/src/sidebar-nav/model.test.ts +++ b/packages/app/src/sidebar-nav/model.test.ts @@ -12,11 +12,11 @@ import { function group(pluginId: string, contributionId: string): PluginSidebarGroup { return { + kind: "item", key: `${pluginId}/sidebar/${contributionId}`, pluginId, contributionId, title: contributionId, - icon: "puzzle", targets: [], }; } @@ -32,7 +32,11 @@ function summarize(items: readonly SidebarNavItem[]): SidebarNavPreference[] { describe("resolveSidebarNavItems", () => { it("yields builtins then plugins, all visible, when nothing is stored", () => { - const items = resolveSidebarNavItems({ pluginGroups: [kanban, notes], preferences: [] }); + const items = resolveSidebarNavItems({ + section: "header", + pluginGroups: [kanban, notes], + preferences: [], + }); expect(summarize(items)).toEqual([ { key: "new-workspace", visible: true }, @@ -53,6 +57,7 @@ describe("resolveSidebarNavItems", () => { it("keeps the stored order and appends newly available items as visible", () => { const items = resolveSidebarNavItems({ + section: "header", pluginGroups: [notes, kanban], preferences: [ { key: kanbanKey, visible: false }, @@ -73,6 +78,7 @@ describe("resolveSidebarNavItems", () => { it("skips keys that are unknown or not currently available", () => { const items = resolveSidebarNavItems({ + section: "header", pluginGroups: [], preferences: [ { key: notesKey, visible: false }, @@ -91,6 +97,7 @@ describe("resolveSidebarNavItems", () => { it("lets the first of duplicate keys win", () => { const items = resolveSidebarNavItems({ + section: "header", pluginGroups: [], preferences: [ { key: "history", visible: false }, @@ -109,7 +116,11 @@ describe("resolveSidebarNavItems", () => { describe("setSidebarNavItemVisible", () => { it("toggles one item and writes the full resolved order", () => { - const items = resolveSidebarNavItems({ pluginGroups: [kanban], preferences: [] }); + const items = resolveSidebarNavItems({ + section: "header", + pluginGroups: [kanban], + preferences: [], + }); const next = setSidebarNavItemVisible({ items, key: "search", visible: false, previous: [] }); @@ -127,7 +138,11 @@ describe("setSidebarNavItemVisible", () => { { key: notesKey, visible: false }, { key: "history", visible: true }, ]; - const items = resolveSidebarNavItems({ pluginGroups: [], preferences: previous }); + const items = resolveSidebarNavItems({ + section: "header", + pluginGroups: [], + preferences: previous, + }); const next = setSidebarNavItemVisible({ items, key: "history", visible: false, previous }); @@ -148,7 +163,11 @@ describe("setSidebarNavItemVisible", () => { { key: "search", visible: true }, { key: "schedules", visible: true }, ]; - const items = resolveSidebarNavItems({ pluginGroups: [], preferences: previous }); + const items = resolveSidebarNavItems({ + section: "header", + pluginGroups: [], + preferences: previous, + }); const next = setSidebarNavItemVisible({ items, key: "history", visible: false, previous }); @@ -159,13 +178,15 @@ describe("setSidebarNavItemVisible", () => { { key: "search", visible: true }, { key: "schedules", visible: true }, ]); - expect(summarize(resolveSidebarNavItems({ pluginGroups: [notes], preferences: next }))).toEqual( - next, - ); + expect( + summarize( + resolveSidebarNavItems({ section: "header", pluginGroups: [notes], preferences: next }), + ), + ).toEqual(next); }); it("returns the normalized list unchanged for an unknown key", () => { - const items = resolveSidebarNavItems({ pluginGroups: [], preferences: [] }); + const items = resolveSidebarNavItems({ section: "header", pluginGroups: [], preferences: [] }); const next = setSidebarNavItemVisible({ items, key: "bogus", visible: false, previous: [] }); @@ -174,7 +195,11 @@ describe("setSidebarNavItemVisible", () => { }); describe("moveSidebarNavItem", () => { - const items = resolveSidebarNavItems({ pluginGroups: [kanban], preferences: [] }); + const items = resolveSidebarNavItems({ + section: "header", + pluginGroups: [kanban], + preferences: [], + }); it("moves an item up", () => { const next = moveSidebarNavItem({ items, key: "search", direction: "up", previous: [] }); @@ -241,3 +266,78 @@ describe("builtinSidebarNavShortcutAction", () => { expect(builtinSidebarNavShortcutAction("schedules")).toBeNull(); }); }); + +describe("footer section", () => { + const sync = group("sync", "status"); + const syncKey = pluginSidebarNavKey(sync); + + it("resolves the Usage item first, then plugin rows", () => { + const items = resolveSidebarNavItems({ + section: "footer", + pluginGroups: [sync], + preferences: [], + }); + + expect(summarize(items)).toEqual([ + { key: "usage", visible: true }, + { key: syncKey, visible: true }, + ]); + }); + + it("ignores header built-ins stored in footer preferences", () => { + const items = resolveSidebarNavItems({ + section: "footer", + pluginGroups: [], + preferences: [{ key: "history", visible: false }], + }); + + expect(summarize(items)).toEqual([{ key: "usage", visible: true }]); + }); + + it("ignores the footer icon buttons, which are fixed and not items", () => { + const items = resolveSidebarNavItems({ + section: "footer", + pluginGroups: [sync], + preferences: [ + { key: "hosts", visible: false }, + { key: "import", visible: false }, + { key: "help", visible: false }, + { key: syncKey, visible: true }, + { key: "usage", visible: false }, + ], + }); + + expect(summarize(items)).toEqual([ + { key: syncKey, visible: true }, + { key: "usage", visible: false }, + ]); + }); + + it("moves and hides footer rows while keeping an unavailable plugin's entry", () => { + const notesPreference = { key: notesKey, visible: false }; + const previous: SidebarNavPreference[] = [notesPreference]; + const items = resolveSidebarNavItems({ + section: "footer", + pluginGroups: [sync], + preferences: previous, + }); + + const moved = moveSidebarNavItem({ items, key: syncKey, direction: "up", previous }); + const hidden = setSidebarNavItemVisible({ + items: resolveSidebarNavItems({ + section: "footer", + pluginGroups: [sync], + preferences: moved, + }), + key: "usage", + visible: false, + previous: moved, + }); + + expect(hidden).toEqual([ + notesPreference, + { key: syncKey, visible: true }, + { key: "usage", visible: false }, + ]); + }); +}); diff --git a/packages/app/src/sidebar-nav/model.ts b/packages/app/src/sidebar-nav/model.ts index f3dbd33520..9691ec8b80 100644 --- a/packages/app/src/sidebar-nav/model.ts +++ b/packages/app/src/sidebar-nav/model.ts @@ -1,7 +1,20 @@ import type { PluginSidebarGroup } from "@/plugins/sidebar-groups"; +import type { PluginSidebarSection } from "@/plugins/types"; -export const BUILTIN_SIDEBAR_NAV_IDS = ["new-workspace", "history", "search", "schedules"] as const; -export type BuiltinSidebarNavId = (typeof BUILTIN_SIDEBAR_NAV_IDS)[number]; +export type SidebarSection = PluginSidebarSection; + +/** + * Each section's built-in items in their default order. The footer's Add project row and its + * icon row (Hosts, Import session, Help and support, Settings) are fixed and not items. + */ +export const BUILTIN_SIDEBAR_ITEM_IDS = { + header: ["new-workspace", "history", "search", "schedules"], + footer: ["usage"], +} as const satisfies Record; + +export type BuiltinSidebarItemId
= + (typeof BUILTIN_SIDEBAR_ITEM_IDS)[Section][number]; +export type BuiltinSidebarNavId = BuiltinSidebarItemId<"header">; /** Persisted shape. Array order is the display order. */ export interface SidebarNavPreference { @@ -9,10 +22,10 @@ export interface SidebarNavPreference { visible: boolean; } -export interface BuiltinSidebarNavItem { +export interface BuiltinSidebarNavItem
{ kind: "builtin"; - key: BuiltinSidebarNavId; - id: BuiltinSidebarNavId; + key: BuiltinSidebarItemId
; + id: BuiltinSidebarItemId
; visible: boolean; } @@ -23,16 +36,19 @@ export interface PluginSidebarNavItem { visible: boolean; } -export type SidebarNavItem = BuiltinSidebarNavItem | PluginSidebarNavItem; +export type SidebarNavItem
= + | BuiltinSidebarNavItem
+ | PluginSidebarNavItem; -const BUILTIN_LABEL_KEYS: Record = { +const BUILTIN_LABEL_KEYS: Record = { "new-workspace": "sidebar.actions.newWorkspace", history: "sidebar.sections.sessions", search: "sidebar.sections.search", schedules: "sidebar.sections.schedules", + usage: "sidebar.footer.usage", }; -export function builtinSidebarNavLabelKey(id: BuiltinSidebarNavId): string { +export function builtinSidebarNavLabelKey(id: BuiltinSidebarItemId): string { return BUILTIN_LABEL_KEYS[id]; } @@ -41,14 +57,15 @@ export function builtinSidebarNavLabelKey(id: BuiltinSidebarNavId): string { * Both the sidebar row and the Appearance settings row read the badge from here so the * two never disagree about which shortcut belongs to which item. */ -const BUILTIN_SHORTCUT_ACTIONS: Record = { +const BUILTIN_SHORTCUT_ACTIONS: Record = { "new-workspace": "new-workspace", history: null, search: "toggle-command-center", schedules: null, + usage: null, }; -export function builtinSidebarNavShortcutAction(id: BuiltinSidebarNavId): string | null { +export function builtinSidebarNavShortcutAction(id: BuiltinSidebarItemId): string | null { return BUILTIN_SHORTCUT_ACTIONS[id]; } @@ -58,18 +75,25 @@ export function pluginSidebarNavKey( return `plugin:${group.pluginId}:${group.contributionId}`; } -function isBuiltinSidebarNavId(key: string): key is BuiltinSidebarNavId { - return (BUILTIN_SIDEBAR_NAV_IDS as readonly string[]).includes(key); +function isBuiltinSidebarItemId
( + section: Section, + key: string, +): key is BuiltinSidebarItemId
{ + const ids: readonly string[] = BUILTIN_SIDEBAR_ITEM_IDS[section]; + return ids.includes(key); } -export function resolveSidebarNavItems(input: { +export function resolveSidebarNavItems
(input: { + section: Section; pluginGroups: readonly PluginSidebarGroup[]; preferences: readonly SidebarNavPreference[]; -}): SidebarNavItem[] { +}): SidebarNavItem
[] { + const builtinIds: readonly BuiltinSidebarItemId
[] = + BUILTIN_SIDEBAR_ITEM_IDS[input.section]; const groupsByKey = new Map( input.pluginGroups.map((group) => [pluginSidebarNavKey(group), group] as const), ); - const items: SidebarNavItem[] = []; + const items: SidebarNavItem
[] = []; const placed = new Set(); for (const preference of input.preferences) { @@ -78,7 +102,7 @@ export function resolveSidebarNavItems(input: { if (group) { placed.add(preference.key); items.push({ kind: "plugin", key: preference.key, group, visible: preference.visible }); - } else if (isBuiltinSidebarNavId(preference.key)) { + } else if (isBuiltinSidebarItemId(input.section, preference.key)) { placed.add(preference.key); items.push({ kind: "builtin", @@ -89,7 +113,7 @@ export function resolveSidebarNavItems(input: { } } - for (const id of BUILTIN_SIDEBAR_NAV_IDS) { + for (const id of builtinIds) { if (placed.has(id)) continue; items.push({ kind: "builtin", key: id, id, visible: true }); } diff --git a/packages/app/src/sidebar-nav/use-sidebar-nav-items.ts b/packages/app/src/sidebar-nav/use-sidebar-nav-items.ts index 634b9f96e7..dcebf670df 100644 --- a/packages/app/src/sidebar-nav/use-sidebar-nav-items.ts +++ b/packages/app/src/sidebar-nav/use-sidebar-nav-items.ts @@ -1,70 +1,74 @@ import { useCallback, useMemo } from "react"; import { useAppSettings } from "@/hooks/use-settings"; +import type { AppSettings } from "@/hooks/use-settings/storage"; import { useInstalledPlugins } from "@/plugins/registry"; -import { groupPluginSidebarContributions } from "@/plugins/sidebar-groups"; +import { groupPluginSidebarItems } from "@/plugins/sidebar-groups"; import { moveSidebarNavItem, resolveSidebarNavItems, setSidebarNavItemVisible, type SidebarNavItem, + type SidebarSection, } from "./model"; -export interface UseSidebarNavItemsReturn { - /** Every top-level item in display order, hidden ones included. */ - items: SidebarNavItem[]; +const PREFERENCE_FIELDS = { + header: "sidebarNavItems", + footer: "sidebarFooterItems", +} as const satisfies Record; + +export interface UseSidebarNavItemsReturn
{ + /** Every item in the section in display order, hidden ones included. */ + items: SidebarNavItem
[]; setVisible: (key: string, visible: boolean) => void; move: (key: string, direction: "up" | "down") => void; } -export function useSidebarNavItems(): UseSidebarNavItemsReturn { +export function useSidebarNavItems
( + section: Section, +): UseSidebarNavItemsReturn
{ const plugins = useInstalledPlugins(); const { settings, updateSettings } = useAppSettings(); - const preferences = settings.sidebarNavItems; - const pluginGroups = useMemo(() => groupPluginSidebarContributions(plugins), [plugins]); + const field = PREFERENCE_FIELDS[section]; + const preferences = settings[field]; + const pluginGroups = useMemo(() => groupPluginSidebarItems(plugins, section), [plugins, section]); const items = useMemo( - () => - resolveSidebarNavItems({ - pluginGroups, - preferences, - }), - [pluginGroups, preferences], + () => resolveSidebarNavItems({ section, pluginGroups, preferences }), + [pluginGroups, preferences, section], ); const setVisible = useCallback( (key: string, visible: boolean) => { void updateSettings((current) => { - const previous = current.sidebarNavItems; - const currentItems = resolveSidebarNavItems({ pluginGroups, preferences: previous }); + const previous = current[field]; + const currentItems = resolveSidebarNavItems({ + section, + pluginGroups, + preferences: previous, + }); return { - sidebarNavItems: setSidebarNavItemVisible({ - items: currentItems, - key, - visible, - previous, - }), + [field]: setSidebarNavItemVisible({ items: currentItems, key, visible, previous }), }; }); }, - [pluginGroups, updateSettings], + [field, pluginGroups, section, updateSettings], ); const move = useCallback( (key: string, direction: "up" | "down") => { void updateSettings((current) => { - const previous = current.sidebarNavItems; - const currentItems = resolveSidebarNavItems({ pluginGroups, preferences: previous }); + const previous = current[field]; + const currentItems = resolveSidebarNavItems({ + section, + pluginGroups, + preferences: previous, + }); return { - sidebarNavItems: moveSidebarNavItem({ - items: currentItems, - key, - direction, - previous, - }), + [field]: moveSidebarNavItem({ items: currentItems, key, direction, previous }), }; }); }, - [pluginGroups, updateSettings], + [field, pluginGroups, section, updateSettings], ); return { items, setVisible, move }; diff --git a/packages/app/src/usage/card.tsx b/packages/app/src/usage/card.tsx index 9fd9292ad3..5175c76224 100644 --- a/packages/app/src/usage/card.tsx +++ b/packages/app/src/usage/card.tsx @@ -1,24 +1,25 @@ -import { RefreshCw } from "lucide-react-native"; -import { useMemo } from "react"; +import { RotateCw } from "lucide-react-native"; +import { useCallback, useMemo } from "react"; import { Text, View, type StyleProp, type TextStyle } from "react-native"; import { StyleSheet, withUnistyles } from "react-native-unistyles"; import { + extraMutedIconColorMapping, iconButtonChromeGlyphSize, - mutedIconColorMapping, smallIconButtonChromeFrameSize, } from "@/components/ui/icon-button-chrome"; import { LoadingSpinner } from "@/components/ui/loading-spinner"; -import { ToolbarButton } from "@/components/ui/pane-content-toolbar"; +import { ToolbarButton, paneContentToolbarIconSize } from "@/components/ui/pane-content-toolbar"; import { StatusBadge } from "@/components/ui/status-badge"; import { useIsCompactFormFactor } from "@/constants/layout"; import { isNative } from "@/constants/platform"; import { useCompactTimeAgo } from "@/hooks/use-time-ago"; import { UsageBalanceBar } from "./balance-bar"; import { usageCopy } from "./copy"; +import type { UsageDisplay } from "./display"; import { formatUsageFreshness, type UsageRefresh } from "./model"; import { useReportRefresh } from "./queries"; import { UsageSourceIcon } from "./source-icon"; -import type { UsageReport, UsageReportEntry } from "./types"; +import type { UsageReport, UsageReportEntry, UsageWindow } from "./types"; import { UsageWindowBar } from "./window-bar"; function statusText(report: UsageReport): string | null { @@ -26,16 +27,18 @@ function statusText(report: UsageReport): string | null { return report.status === "error" ? "Error" : "Unavailable"; } -const ThemedRefreshIcon = withUnistyles(RefreshCw); +const ThemedRotateCw = withUnistyles(RotateCw); const ThemedLoadingSpinner = withUnistyles(LoadingSpinner); export function UsageCard({ serverId, entry, + display, compact = false, }: { serverId: string; entry: UsageReportEntry; + display: UsageDisplay; compact?: boolean; }) { const isCompact = useIsCompactFormFactor(); @@ -62,13 +65,13 @@ export function UsageCard({ ); return ( - + {entry.sourceLabel} - {usage.planLabel ? : null} + {usage.planLabel ? : null} {status ? ( @@ -94,7 +97,7 @@ export function UsageCard({ {usage.windows.length > 0 || balances.length > 0 ? ( {usage.windows.map((window) => ( - + ))} {balances.map((balance) => ( @@ -141,6 +144,33 @@ export function UsageCard({ ); } +function PinnableWindowBar({ + entry, + window, + display, +}: { + entry: UsageReportEntry; + window: UsageWindow; + display: UsageDisplay; +}) { + const pin = useMemo( + () => ({ sourceId: entry.sourceId, windowId: window.id }), + [entry.sourceId, window.id], + ); + const { togglePin } = display; + const toggle = useCallback(() => togglePin(pin), [pin, togglePin]); + return ( + + ); +} + /** Refreshes this one report. Its tooltip says when the report on screen was fetched. */ function UsageRefreshButton({ sourceLabel, @@ -156,7 +186,7 @@ function UsageRefreshButton({ compact: boolean; }) { const isPending = refreshState === "pending"; - const iconSize = iconButtonChromeGlyphSize("small", compact); + const iconSize = paneContentToolbarIconSize(compact); const freshness = useMemo( () => ( {isPending ? ( - + ) : ( - + )} ); @@ -217,8 +247,9 @@ const styles = StyleSheet.create((theme) => ({ container: { gap: theme.spacing[3], }, + // The gap is one step under the card padding: window rows add their own vertical padding. containerPadded: { - gap: theme.spacing[4], + gap: theme.spacing[3], paddingVertical: theme.spacing[4], paddingHorizontal: theme.spacing[4], }, @@ -259,8 +290,9 @@ const styles = StyleSheet.create((theme) => ({ color: theme.colors.foregroundMuted, fontSize: theme.fontSize.sm, }, + // Window rows carry their own vertical padding, which already separates them. bars: { - gap: theme.spacing[3], + gap: theme.spacing[1], }, details: { gap: theme.spacing[1], diff --git a/packages/app/src/usage/composer-pill.tsx b/packages/app/src/usage/composer-pill.tsx deleted file mode 100644 index 26cd77793c..0000000000 --- a/packages/app/src/usage/composer-pill.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { Text, View } from "react-native"; -import { StyleSheet } from "react-native-unistyles"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import type { MenuTriggerState } from "@/components/ui/menu"; -import { - isToolbarLabelTriggerHighlighted, - toolbarLabelTriggerStyle, - toolbarLabelTriggerTextStyle, -} from "@/components/ui/toolbar-label-trigger"; -import { UsageCard } from "./card"; -import { usageCopy } from "./copy"; -import type { UsagePill } from "./model"; -import { useAgentUsage } from "./queries"; -import { UsageSourceIcon } from "./source-icon"; - -function pillTriggerStyle(state: MenuTriggerState) { - return [toolbarLabelTriggerStyle(state), styles.trigger]; -} - -/** - * The agent's plan usage beside the composer controls. Opens the cached usage card; - * only the card's Refresh asks the source for a new report. - */ -export function UsageComposerPill({ serverId, agentId }: { serverId: string; agentId: string }) { - const { pill, entry } = useAgentUsage(serverId, agentId); - - if (!pill || !entry) return null; - - return ( - - - {(state) => ( - - )} - - - - - - - - ); -} - -function UsagePillContent({ pill, highlighted }: { pill: UsagePill; highlighted: boolean }) { - return ( - <> - - {pill.text ? ( - - {pill.text} - - ) : null} - - ); -} - -const styles = StyleSheet.create((theme) => ({ - trigger: { - height: 28, - flexShrink: 0, - }, - popover: { - padding: theme.spacing[3], - }, -})); diff --git a/packages/app/src/usage/controls.tsx b/packages/app/src/usage/controls.tsx new file mode 100644 index 0000000000..582f35102b --- /dev/null +++ b/packages/app/src/usage/controls.tsx @@ -0,0 +1,111 @@ +import { RotateCw } from "lucide-react-native"; +import { View } from "react-native"; +import { StyleSheet, withUnistyles } from "react-native-unistyles"; +import { HostSwitcher } from "@/components/hosts/host-switcher"; +import { extraMutedIconColorMapping } from "@/components/ui/icon-button-chrome"; +import { LoadingSpinner } from "@/components/ui/loading-spinner"; +import { ToolbarButton, paneContentToolbarIconSize } from "@/components/ui/pane-content-toolbar"; +import { useIsCompactFormFactor } from "@/constants/layout"; +import { useMemo, type ReactElement } from "react"; +import { usageCopy } from "./copy"; +import type { UsageDisplay } from "./display"; +import { UsageDisplayToggle } from "./display-toggle"; +import type { UsageHost } from "./model"; +import { useHostUsage } from "./queries"; +import type { UsageView } from "./types"; + +const ThemedRotateCw = withUnistyles(RotateCw); +const ThemedLoadingSpinner = withUnistyles(LoadingSpinner); + +/** The hosts to choose between, and which one is shown. */ +export interface UsageHostSelection { + hosts: UsageHost[]; + serverId: string; + onSelect: (serverId: string) => void; +} + +/** + * The controls on the right of every usage title row: the host selector, which also names the + * host shown, the used/remaining toggle and Refresh. A host that cannot report usage keeps only + * the selector. + */ +export function UsageControls({ + view, + display, + onRefresh, + hostSelection, +}: { + view: UsageView; + display: UsageDisplay; + onRefresh: () => void; + hostSelection?: UsageHostSelection; +}) { + const busy = view.kind === "loading" || (view.kind === "ready" && view.isRefreshing); + const compact = useIsCompactFormFactor(); + const iconSize = paneContentToolbarIconSize(compact); + return ( + + {hostSelection && hostSelection.hosts.length > 0 ? ( + + ) : null} + {view.kind === "unavailable" ? null : ( + <> + + {/* The Changes panel's refresh: an icon, with the label in its tooltip. */} + + {busy ? ( + + ) : ( + + )} + + + )} + + ); +} + +const styles = StyleSheet.create((theme) => ({ + controls: { + flexShrink: 1, + flexDirection: "row", + alignItems: "center", + gap: theme.spacing[2], + }, +})); + +/** + * One host's usage and the title-row controls that go with it, for the Usage screen and the + * compact usage sheet. + */ +export function useHostUsageWithControls( + hostSelection: UsageHostSelection, + display: UsageDisplay, +): { view: UsageView; refresh: () => void; controls: ReactElement } { + const { view, refresh } = useHostUsage(hostSelection.serverId); + const controls = useMemo( + () => ( + + ), + [display, hostSelection, refresh, view], + ); + return { view, refresh, controls }; +} diff --git a/packages/app/src/usage/copy.ts b/packages/app/src/usage/copy.ts index ba0c8b0a46..ec42581fbe 100644 --- a/packages/app/src/usage/copy.ts +++ b/packages/app/src/usage/copy.ts @@ -11,8 +11,12 @@ export const usageCopy = { empty: "No usage data", noHosts: "No connected hosts", errorTitle: "Unable to load usage", - hostUnavailable: "Connect to this host to see usage", - hostUpgradeRequired: "Update the host to see usage", + hostUnavailable: (host: string) => `Connect to ${host} to see usage`, + hostUpgradeRequired: (host: string) => `Update ${host} to see usage`, clientUnavailable: "Host connection is not ready", retry: "Try again", + pin: "Pin", + host: "Usage host", + displayUsed: "Used", + displayRemaining: "Remaining", } as const; diff --git a/packages/app/src/usage/display-toggle.tsx b/packages/app/src/usage/display-toggle.tsx new file mode 100644 index 0000000000..f778172b7c --- /dev/null +++ b/packages/app/src/usage/display-toggle.tsx @@ -0,0 +1,22 @@ +import { SegmentedControl, type SegmentedControlOption } from "@/components/ui/segmented-control"; +import { usageCopy } from "./copy"; +import type { UsageDisplay } from "./display"; +import type { UsageDisplayAs } from "./preferences"; + +const OPTIONS: SegmentedControlOption[] = [ + { value: "used", label: usageCopy.displayUsed, testID: "usage-display-used" }, + { value: "remaining", label: usageCopy.displayRemaining, testID: "usage-display-remaining" }, +]; + +/** Switches every usage surface between the share used and the share left. */ +export function UsageDisplayToggle({ display }: { display: UsageDisplay }) { + return ( + + ); +} diff --git a/packages/app/src/usage/display.ts b/packages/app/src/usage/display.ts new file mode 100644 index 0000000000..09fdb3d8ea --- /dev/null +++ b/packages/app/src/usage/display.ts @@ -0,0 +1,46 @@ +import { useCallback, useMemo } from "react"; +import { useAppSettings } from "@/hooks/use-settings"; +import { + isUsagePinned, + setUsageDisplayAs, + toggleUsagePin, + type UsageDisplayAs, + type UsagePin, + type UsagePreferences, +} from "./preferences"; + +/** What usage views need from the preferences: how to read percents, and the pins. */ +export interface UsageDisplay { + displayAs: UsageDisplayAs; + setDisplayAs: (displayAs: UsageDisplayAs) => void; + isPinned: (pin: UsagePin) => boolean; + togglePin: (pin: UsagePin) => void; +} + +/** The device's usage preferences, and the commands that change them. */ +export function useUsagePreferences(): { preferences: UsagePreferences; display: UsageDisplay } { + const { settings, updateSettings } = useAppSettings(); + const preferences = settings.usage; + const setDisplayAs = useCallback( + (displayAs: UsageDisplayAs) => { + void updateSettings((current) => ({ usage: setUsageDisplayAs(current.usage, displayAs) })); + }, + [updateSettings], + ); + const togglePin = useCallback( + (pin: UsagePin) => { + void updateSettings((current) => ({ usage: toggleUsagePin(current.usage, pin) })); + }, + [updateSettings], + ); + const display = useMemo( + () => ({ + displayAs: preferences.displayAs, + setDisplayAs, + isPinned: (pin) => isUsagePinned(preferences, pin), + togglePin, + }), + [preferences, setDisplayAs, togglePin], + ); + return { preferences, display }; +} diff --git a/packages/app/src/usage/format.ts b/packages/app/src/usage/format.ts index ac8bffefd0..253586c173 100644 --- a/packages/app/src/usage/format.ts +++ b/packages/app/src/usage/format.ts @@ -1,4 +1,5 @@ import { formatTokenCount } from "@/components/context-window-meter.utils"; +import type { UsageDisplayAs } from "./preferences"; import type { UsageBalanceUnit } from "./types"; export function clampPct(value: number): number { @@ -9,6 +10,11 @@ export function formatPct(value: number): string { return `${Math.round(clampPct(value))}%`; } +/** "31%" of the window used, or "69% left" of it. */ +export function formatDisplayPct(value: number, displayAs: UsageDisplayAs): string { + return displayAs === "used" ? formatPct(value) : `${formatPct(value)} left`; +} + function relativeDuration(iso: string): string | null { const diffMs = new Date(iso).getTime() - Date.now(); if (!Number.isFinite(diffMs)) return null; diff --git a/packages/app/src/usage/host-usage-section.tsx b/packages/app/src/usage/host-usage-section.tsx index 54dd4ef939..f3cb0af3b0 100644 --- a/packages/app/src/usage/host-usage-section.tsx +++ b/packages/app/src/usage/host-usage-section.tsx @@ -1,15 +1,18 @@ import { usageCopy } from "./copy"; +import { useUsagePreferences } from "./display"; import { useHostUsage } from "./queries"; import { UsageSection } from "./usage-section"; /** A host's usage reports, for its settings page. */ export function HostUsageSection({ serverId }: { serverId: string }) { const { view, refresh } = useHostUsage(serverId); + const { display } = useUsagePreferences(); return ( diff --git a/packages/app/src/usage/hosts.ts b/packages/app/src/usage/hosts.ts new file mode 100644 index 0000000000..5b6597ecdd --- /dev/null +++ b/packages/app/src/usage/hosts.ts @@ -0,0 +1,39 @@ +import { useMemo, useState } from "react"; +import { + useActiveWorkspaceSelection, + useLastWorkspaceSelection, +} from "@/stores/navigation-active-workspace-store"; +import { resolveUsageHostId, resolveUsageScreenHostId, type UsageHost } from "./model"; +import { useUsageHosts } from "./queries"; + +/** The active workspace's host; off a workspace route, the last workspace visited. */ +function useActiveServerId(): string | null { + const active = useActiveWorkspaceSelection(); + const last = useLastWorkspaceSelection(); + return active?.serverId ?? last?.serverId ?? null; +} + +/** The host the sidebar Usage row reads, or null when none reports usage. */ +export function useUsageHostId(): string | null { + return resolveUsageHostId(useActiveServerId(), useUsageHosts()); +} + +/** + * The host the Usage screen or the compact usage sheet shows, and the hosts to pick from. The pick + * is the view's state: it resets when the view unmounts. + */ +export function useUsageHostSelection(): { + serverId: string | null; + connectedHosts: UsageHost[]; + select: (serverId: string) => void; +} { + const hosts = useUsageHosts(); + const activeServerId = useActiveServerId(); + const [selectedServerId, select] = useState(null); + const connectedHosts = useMemo(() => hosts.filter((host) => host.isConnected), [hosts]); + return { + serverId: resolveUsageScreenHostId({ selectedServerId, activeServerId, hosts }), + connectedHosts, + select, + }; +} diff --git a/packages/app/src/usage/index.ts b/packages/app/src/usage/index.ts index 54f745a04d..6ab6c1447d 100644 --- a/packages/app/src/usage/index.ts +++ b/packages/app/src/usage/index.ts @@ -1,3 +1,3 @@ -export { UsageComposerPill } from "./composer-pill"; export { HostUsageSection } from "./host-usage-section"; export { UsageScreen } from "./usage-screen"; +export { UsageSidebarItem } from "./sidebar-item"; diff --git a/packages/app/src/usage/list.tsx b/packages/app/src/usage/list.tsx index 91aa4d4b50..f0c23dd0f7 100644 --- a/packages/app/src/usage/list.tsx +++ b/packages/app/src/usage/list.tsx @@ -1,32 +1,33 @@ -import { Fragment } from "react"; import { View } from "react-native"; import { StyleSheet } from "react-native-unistyles"; import { settingsStyles } from "@/styles/settings"; import { UsageCard } from "./card"; +import type { UsageDisplay } from "./display"; import type { UsageReportEntry } from "./types"; +/** One card per report (source + account). */ export function UsageList({ serverId, reports, + display, }: { serverId: string; reports: UsageReportEntry[]; + display: UsageDisplay; }) { return ( - - {reports.map((entry, index) => ( - - {index > 0 ? : null} - - + + {reports.map((entry) => ( + + + ))} ); } const styles = StyleSheet.create((theme) => ({ - divider: { - height: 1, - backgroundColor: theme.colors.border, + list: { + gap: theme.spacing[3], }, })); diff --git a/packages/app/src/usage/model.test.ts b/packages/app/src/usage/model.test.ts index 54ebc4df87..fd46a2e476 100644 --- a/packages/app/src/usage/model.test.ts +++ b/packages/app/src/usage/model.test.ts @@ -1,12 +1,14 @@ import { describe, expect, it } from "vitest"; import { + displayPercent, formatUsageFreshness, - groupUsageByHost, replaceReport, - headlineWindow, - resolveUsagePill, resolveUsageRefresh, + resolveUsageHostId, + resolveUsageScreenHostId, resolveUsageView, + usageWindowRowLabel, + type UsageHost, type UsageQueryState, } from "./model"; import type { UsageReportEntry, UsageWindow } from "./types"; @@ -36,85 +38,31 @@ function ready(data: UsageReportEntry[]): UsageQueryState { return { data, error: null, isFetching: false }; } -describe("resolveUsagePill", () => { - it("shows the source icon and the headline window percent", () => { - const pill = resolveUsagePill({ - supportsUsage: true, - entry: entry({ - icon: "", - windows: [ - { id: "session", label: "Session", usedPct: 12 }, - { id: "weekly", label: "Weekly", usedPct: 64.6, headline: true }, - ], - }), - }); - - expect(pill).toEqual({ icon: "", sourceLabel: "Fixture source", text: "65%" }); - }); - - it("derives the headline percent from remaining percent", () => { - const pill = resolveUsagePill({ - supportsUsage: true, - entry: entry({ - windows: [{ id: "daily", label: "Daily", remainingPct: 30, headline: true }], - }), - }); - - expect(pill?.text).toBe("70%"); - }); - - it("shows the plan label when no window is the headline", () => { - const pill = resolveUsagePill({ - supportsUsage: true, - entry: entry({ - planLabel: "Pro", - windows: [{ id: "session", label: "Session", usedPct: 40 }], - }), - }); - - expect(pill).toEqual({ icon: null, sourceLabel: "Fixture source", text: "Pro" }); - }); - - it("shows only the icon with neither a headline window nor a plan label", () => { - const pill = resolveUsagePill({ supportsUsage: true, entry: entry({}) }); - - expect(pill).toEqual({ icon: null, sourceLabel: "Fixture source", text: null }); - }); - - it("is hidden when the agent has no usage report", () => { - expect(resolveUsagePill({ supportsUsage: true, entry: null })).toBeNull(); - }); - - it("is hidden on a host without usage sources, whatever data is cached", () => { - const cached = entry({ windows: [{ id: "w", label: "W", usedPct: 5, headline: true }] }); - - expect(resolveUsagePill({ supportsUsage: false, entry: cached })).toBeNull(); - }); -}); - -describe("headlineWindow", () => { - it("does not fall back to the first window", () => { - expect(headlineWindow(entry({ windows: [{ id: "a", label: "A", usedPct: 1 }] }).report)).toBe( - null, - ); - }); -}); - describe("resolveUsageView", () => { it("asks for a host update when the host lacks usage sources", () => { expect( - resolveUsageView({ isConnected: true, supportsUsage: false, query: ready([entry({})]) }), - ).toEqual({ kind: "unavailable", message: "Update the host to see usage" }); + resolveUsageView({ + hostLabel: "Laptop", + isConnected: true, + supportsUsage: false, + query: ready([entry({})]), + }), + ).toEqual({ kind: "unavailable", message: "Update Laptop to see usage" }); }); it("asks for a connection before anything else", () => { expect( - resolveUsageView({ isConnected: false, supportsUsage: false, query: undefined }), - ).toEqual({ kind: "unavailable", message: "Connect to this host to see usage" }); + resolveUsageView({ + hostLabel: "Laptop", + isConnected: false, + supportsUsage: false, + query: undefined, + }), + ).toEqual({ kind: "unavailable", message: "Connect to Laptop to see usage" }); }); it("moves from loading to ready to error", () => { - const base = { isConnected: true, supportsUsage: true }; + const base = { hostLabel: "Laptop", isConnected: true, supportsUsage: true }; expect(resolveUsageView({ ...base, query: undefined })).toEqual({ kind: "loading" }); expect( resolveUsageView({ ...base, query: { data: [], error: null, isFetching: true } }), @@ -128,49 +76,95 @@ describe("resolveUsageView", () => { }); }); -describe("groupUsageByHost", () => { - it("groups reports under each connected host in host order", () => { - const first = entry({ sourceId: "one" }); - const second = entry({ sourceId: "two" }); - const groups = groupUsageByHost( - [ - { serverId: "b", label: "Beta", isConnected: true, supportsUsage: true }, - { serverId: "offline", label: "Offline", isConnected: false, supportsUsage: true }, - { serverId: "a", label: "Alpha", isConnected: true, supportsUsage: true }, - ], - new Map([ - ["a", ready([first])], - ["b", ready([second, first])], - ]), - ); +const hosts: UsageHost[] = [ + { serverId: "offline", label: "Offline", isConnected: false, supportsUsage: true }, + { serverId: "old", label: "Old", isConnected: true, supportsUsage: false }, + { serverId: "a", label: "Alpha", isConnected: true, supportsUsage: true }, + { serverId: "b", label: "Beta", isConnected: true, supportsUsage: true }, +]; - expect(groups).toEqual([ - { - serverId: "b", - label: "Beta", - view: { kind: "ready", reports: [second, first], isRefreshing: false }, - }, - { - serverId: "a", - label: "Alpha", - view: { kind: "ready", reports: [first], isRefreshing: false }, - }, - ]); +describe("resolveUsageHostId", () => { + it("reads the active workspace's host", () => { + expect(resolveUsageHostId("b", hosts)).toBe("b"); }); - it("shows the update message in an old host's group", () => { - const groups = groupUsageByHost( - [ - { serverId: "new", label: "New", isConnected: true, supportsUsage: true }, - { serverId: "old", label: "Old", isConnected: true, supportsUsage: false }, - ], - new Map([["new", ready([])]]), - ); + it("falls back to the first connected host that reports usage", () => { + expect(resolveUsageHostId(null, hosts)).toBe("a"); + expect(resolveUsageHostId("offline", hosts)).toBe("a"); + expect(resolveUsageHostId("old", hosts)).toBe("a"); + }); - expect(groups.map((group) => [group.label, group.view])).toEqual([ - ["New", { kind: "ready", reports: [], isRefreshing: false }], - ["Old", { kind: "unavailable", message: "Update the host to see usage" }], - ]); + it("has no host when none reports usage", () => { + expect(resolveUsageHostId("old", hosts.slice(0, 2))).toBeNull(); + }); +}); + +describe("resolveUsageScreenHostId", () => { + it("keeps the user's pick while it stays connected", () => { + expect(resolveUsageScreenHostId({ selectedServerId: "old", activeServerId: "b", hosts })).toBe( + "old", + ); + expect( + resolveUsageScreenHostId({ selectedServerId: "offline", activeServerId: "b", hosts }), + ).toBe("b"); + }); + + it("defaults to the host the sidebar row reads", () => { + for (const activeServerId of ["b", null, "old", "offline"]) { + expect(resolveUsageScreenHostId({ selectedServerId: null, activeServerId, hosts })).toBe( + resolveUsageHostId(activeServerId, hosts), + ); + } + expect(resolveUsageScreenHostId({ selectedServerId: null, activeServerId: "old", hosts })).toBe( + "a", + ); + }); + + it("shows the first connected host when none reports usage, so the screen says to update it", () => { + expect( + resolveUsageScreenHostId({ + selectedServerId: null, + activeServerId: null, + hosts: hosts.slice(0, 2), + }), + ).toBe("old"); + expect( + resolveUsageScreenHostId({ selectedServerId: null, activeServerId: null, hosts: [] }), + ).toBeNull(); + }); +}); + +describe("usageWindowRowLabel", () => { + it("names the window, its percent and its reset after what pinning it pins", () => { + expect( + usageWindowRowLabel({ + pinLabel: "Pin Claude Session", + value: "31%", + trailing: "resets in 2h", + }), + ).toBe("Pin Claude Session, 31% · resets in 2h"); + expect(usageWindowRowLabel({ pinLabel: "Pin Codex Weekly", value: "—", trailing: null })).toBe( + "Pin Codex Weekly, —", + ); + }); +}); + +describe("displayPercent", () => { + const window = (input: Partial): UsageWindow => ({ + id: "weekly", + label: "Weekly", + ...input, + }); + + it("reads the share used or the share left", () => { + expect(displayPercent(window({ usedPct: 31 }), "used")).toBe(31); + expect(displayPercent(window({ usedPct: 31 }), "remaining")).toBe(69); + expect(displayPercent(window({ remainingPct: 40 }), "used")).toBe(60); + expect(displayPercent(window({ usedPct: 50, remainingPct: 45 }), "remaining")).toBe(45); + }); + + it("has no percent when the window reports none", () => { + expect(displayPercent(window({}), "remaining")).toBeNull(); }); }); diff --git a/packages/app/src/usage/model.ts b/packages/app/src/usage/model.ts index c3cddc70fe..ad5fe99c2f 100644 --- a/packages/app/src/usage/model.ts +++ b/packages/app/src/usage/model.ts @@ -1,6 +1,7 @@ import { formatCompactTimeAgoAsProse } from "@/utils/time"; import { usageCopy } from "./copy"; -import type { UsageReport, UsageReportEntry, UsageView, UsageWindow } from "./types"; +import type { UsageDisplayAs } from "./preferences"; +import type { UsageReportEntry, UsageView, UsageWindow } from "./types"; export function usedPercent(window: UsageWindow): number | null { if (window.usedPct != null) return window.usedPct; @@ -8,34 +9,26 @@ export function usedPercent(window: UsageWindow): number | null { return null; } -/** The window the source marked as its headline. Sources own that choice; there is no fallback. */ -export function headlineWindow(report: UsageReport): UsageWindow | null { - return report.windows.find((window) => window.headline === true) ?? null; +/** The percent a window shows under the user's used/remaining preference. */ +export function displayPercent(window: UsageWindow, displayAs: UsageDisplayAs): number | null { + if (displayAs === "used") return usedPercent(window); + if (window.remainingPct != null) return window.remainingPct; + const used = usedPercent(window); + return used == null ? null : 100 - used; } -export interface UsagePill { - icon: string | null; - sourceLabel: string; - /** Headline percent, else the plan label, else nothing beside the icon. */ - text: string | null; -} - -export function resolveUsagePill(input: { - supportsUsage: boolean; - entry: UsageReportEntry | null | undefined; -}): UsagePill | null { - const { supportsUsage, entry } = input; - if (!supportsUsage || !entry) return null; - const window = headlineWindow(entry.report); - const percent = window ? usedPercent(window) : null; - return { - icon: entry.icon ?? null, - sourceLabel: entry.sourceLabel, - text: - percent != null - ? `${Math.round(Math.max(0, Math.min(100, percent)))}%` - : (entry.report.planLabel ?? null), - }; +/** + * A window row's accessible label: what pinning it pins, then what the row shows. The row is a + * checkbox, so its checked state says whether it is pinned: "Pin Claude Session, 31% · resets in + * 2h", checked. + */ +export function usageWindowRowLabel(input: { + pinLabel: string; + value: string; + trailing: string | null | undefined; +}): string { + const summary = input.trailing ? `${input.value} · ${input.trailing}` : input.value; + return `${input.pinLabel}, ${summary}`; } /** When a report was fetched, from its compact relative time: "Updated 3m ago". */ @@ -75,13 +68,16 @@ export interface UsageQueryState { } export function resolveUsageView(input: { + hostLabel: string; isConnected: boolean; supportsUsage: boolean; query: UsageQueryState | undefined; }): UsageView { - const { isConnected, supportsUsage, query } = input; - if (!isConnected) return { kind: "unavailable", message: usageCopy.hostUnavailable }; - if (!supportsUsage) return { kind: "unavailable", message: usageCopy.hostUpgradeRequired }; + const { hostLabel, isConnected, supportsUsage, query } = input; + if (!isConnected) return { kind: "unavailable", message: usageCopy.hostUnavailable(hostLabel) }; + if (!supportsUsage) { + return { kind: "unavailable", message: usageCopy.hostUpgradeRequired(hostLabel) }; + } if (query?.data) { return { kind: "ready", reports: query.data, isRefreshing: query.isFetching }; } @@ -101,26 +97,34 @@ export interface UsageHost { supportsUsage: boolean; } -export interface UsageHostGroup { - serverId: string; - label: string; - view: UsageView; +/** + * The host usage shows by default, on the sidebar row and the Usage screen: the active workspace's + * host if it reports usage, else the first host that does. + */ +export function resolveUsageHostId( + activeServerId: string | null, + hosts: readonly UsageHost[], +): string | null { + const reporting = hosts.filter((host) => host.isConnected && host.supportsUsage); + const active = reporting.find((host) => host.serverId === activeServerId); + return (active ?? reporting[0])?.serverId ?? null; } -/** One group per connected host, in host order. */ -export function groupUsageByHost( - hosts: readonly UsageHost[], - queries: ReadonlyMap, -): UsageHostGroup[] { - return hosts - .filter((host) => host.isConnected) - .map((host) => ({ - serverId: host.serverId, - label: host.label, - view: resolveUsageView({ - isConnected: true, - supportsUsage: host.supportsUsage, - query: queries.get(host.serverId), - }), - })); +/** + * The host the Usage screen shows: the user's pick while it stays connected, else the default + * host. With no host reporting usage, the first connected host, so the screen says to update it. + */ +export function resolveUsageScreenHostId(input: { + selectedServerId: string | null; + activeServerId: string | null; + hosts: readonly UsageHost[]; +}): string | null { + const connected = input.hosts.filter((host) => host.isConnected); + const selected = connected.find((host) => host.serverId === input.selectedServerId); + return ( + selected?.serverId ?? + resolveUsageHostId(input.activeServerId, input.hosts) ?? + connected[0]?.serverId ?? + null + ); } diff --git a/packages/app/src/usage/pinned.test.ts b/packages/app/src/usage/pinned.test.ts new file mode 100644 index 0000000000..0580d3ccfc --- /dev/null +++ b/packages/app/src/usage/pinned.test.ts @@ -0,0 +1,123 @@ +import { describe, expect, it } from "vitest"; +import type { UsagePreferences } from "./preferences"; +import { resolvePinnedUsage } from "./pinned"; +import type { UsageReportEntry, UsageWindow } from "./types"; + +function report(input: { + sourceId: string; + sourceLabel: string; + account?: string; + icon?: string; + windows: UsageWindow[]; +}): UsageReportEntry { + return { + id: `${input.sourceId}:${input.account ?? "default"}`, + account: input.account ? { label: input.account } : {}, + fetchedAt: "2026-01-01T00:00:00.000Z", + sourceId: input.sourceId, + sourceLabel: input.sourceLabel, + ...(input.icon ? { icon: input.icon } : {}), + report: { status: "available", windows: input.windows }, + }; +} + +const claude = report({ + sourceId: "claude", + sourceLabel: "Claude", + icon: "", + windows: [ + { id: "five-hour", label: "5-hour", usedPct: 31 }, + { id: "weekly", label: "Weekly", usedPct: 80 }, + ], +}); +const codex = report({ + sourceId: "codex", + sourceLabel: "Codex", + windows: [{ id: "weekly", label: "Weekly", remainingPct: 88 }], +}); + +function preferences( + pinned: UsagePreferences["pinned"], + displayAs: UsagePreferences["displayAs"] = "used", +): UsagePreferences { + return { displayAs, pinned }; +} + +describe("resolvePinnedUsage", () => { + it("shows pinned windows in pin order, not report order", () => { + const items = resolvePinnedUsage( + [claude, codex], + preferences([ + { sourceId: "codex", windowId: "weekly" }, + { sourceId: "claude", windowId: "five-hour" }, + ]), + ); + + expect(items).toEqual([ + { key: "codex:default/weekly", icon: null, label: "Codex Weekly", percentText: "12%" }, + { + key: "claude:default/five-hour", + icon: "", + label: "Claude 5-hour", + percentText: "31%", + }, + ]); + }); + + it("formats the share left when the user reads usage as remaining", () => { + const items = resolvePinnedUsage( + [claude, codex], + preferences( + [ + { sourceId: "claude", windowId: "five-hour" }, + { sourceId: "codex", windowId: "weekly" }, + ], + "remaining", + ), + ); + + expect(items.map((item) => item.percentText)).toEqual(["69% left", "88% left"]); + }); + + it("leaves out a pinned window that no report has, or that reports no percent", () => { + const noPercent = report({ + sourceId: "opencode", + sourceLabel: "OpenCode", + windows: [{ id: "monthly", label: "Monthly" }], + }); + const items = resolvePinnedUsage( + [claude, noPercent], + preferences([ + { sourceId: "claude", windowId: "monthly" }, + { sourceId: "codex", windowId: "weekly" }, + { sourceId: "opencode", windowId: "monthly" }, + ]), + ); + + expect(items).toEqual([]); + }); + + it("shows a pin once per account of the source", () => { + const personal = report({ + sourceId: "claude", + sourceLabel: "Claude", + account: "personal", + windows: [{ id: "five-hour", label: "5-hour", usedPct: 10 }], + }); + const work = report({ + sourceId: "claude", + sourceLabel: "Claude", + account: "work", + windows: [{ id: "five-hour", label: "5-hour", usedPct: 90 }], + }); + const items = resolvePinnedUsage( + [personal, codex, work], + preferences([{ sourceId: "claude", windowId: "five-hour" }]), + ); + + expect(items.map((item) => [item.label, item.percentText])).toEqual([ + ["Claude (personal) 5-hour", "10%"], + ["Claude (work) 5-hour", "90%"], + ]); + }); +}); diff --git a/packages/app/src/usage/pinned.ts b/packages/app/src/usage/pinned.ts new file mode 100644 index 0000000000..7d348ffcd2 --- /dev/null +++ b/packages/app/src/usage/pinned.ts @@ -0,0 +1,44 @@ +import { formatDisplayPct } from "./format"; +import { displayPercent } from "./model"; +import type { UsagePreferences } from "./preferences"; +import type { UsageReportEntry } from "./types"; + +/** One pinned window of one account, as the sidebar Usage item shows it. */ +export interface PinnedUsageWindow { + key: string; + icon: string | null; + /** Source, account when it has one, and window: "Claude (work) 5-hour". */ + label: string; + percentText: string; +} + +function describe(entry: UsageReportEntry, windowLabel: string): string { + const account = entry.account.label ? ` (${entry.account.label})` : ""; + return `${entry.sourceLabel}${account} ${windowLabel}`; +} + +/** + * The pinned windows the Usage item shows: pins in pin order, and within a pin one item per account of the source + * in report order. A pinned window a report lacks, or reports no percent for, is left out. + */ +export function resolvePinnedUsage( + reports: readonly UsageReportEntry[], + preferences: UsagePreferences, +): PinnedUsageWindow[] { + return preferences.pinned.flatMap((pin) => + reports.flatMap((entry) => { + if (entry.sourceId !== pin.sourceId) return []; + const window = entry.report.windows.find((candidate) => candidate.id === pin.windowId); + const percent = window ? displayPercent(window, preferences.displayAs) : null; + if (!window || percent == null) return []; + return [ + { + key: `${entry.id}/${window.id}`, + icon: entry.icon ?? null, + label: describe(entry, window.label), + percentText: formatDisplayPct(percent, preferences.displayAs), + }, + ]; + }), + ); +} diff --git a/packages/app/src/usage/preferences.test.ts b/packages/app/src/usage/preferences.test.ts new file mode 100644 index 0000000000..b060632067 --- /dev/null +++ b/packages/app/src/usage/preferences.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, it } from "vitest"; +import { + DEFAULT_USAGE_PREFERENCES, + isUsagePinned, + setUsageDisplayAs, + toggleUsagePin, +} from "./preferences"; + +const claudeFiveHour = { sourceId: "claude", windowId: "five-hour" }; +const codexWeekly = { sourceId: "codex", windowId: "weekly" }; + +describe("usage preferences", () => { + it("pins windows in the order the user pins them", () => { + const pinned = toggleUsagePin( + toggleUsagePin(DEFAULT_USAGE_PREFERENCES, codexWeekly), + claudeFiveHour, + ); + + expect(pinned.pinned).toEqual([codexWeekly, claudeFiveHour]); + expect(isUsagePinned(pinned, { sourceId: "codex", windowId: "weekly" })).toBe(true); + expect(isUsagePinned(pinned, { sourceId: "codex", windowId: "five-hour" })).toBe(false); + }); + + it("unpins a pinned window and keeps the others in order", () => { + const three = [codexWeekly, claudeFiveHour, { sourceId: "claude", windowId: "weekly" }]; + const unpinned = toggleUsagePin({ displayAs: "used", pinned: three }, claudeFiveHour); + + expect(unpinned.pinned).toEqual([codexWeekly, { sourceId: "claude", windowId: "weekly" }]); + }); + + it("switches between used and remaining without touching pins", () => { + const preferences = { displayAs: "used" as const, pinned: [codexWeekly] }; + + expect(setUsageDisplayAs(preferences, "remaining")).toEqual({ + displayAs: "remaining", + pinned: [codexWeekly], + }); + }); +}); diff --git a/packages/app/src/usage/preferences.ts b/packages/app/src/usage/preferences.ts new file mode 100644 index 0000000000..55c5ee7d8e --- /dev/null +++ b/packages/app/src/usage/preferences.ts @@ -0,0 +1,48 @@ +import { z } from "zod"; + +/** Whether percentages read as the share used or the share left. */ +export type UsageDisplayAs = "used" | "remaining"; + +/** A window the user pinned to the sidebar Usage item. It matches every account of the source. */ +export interface UsagePin { + sourceId: string; + windowId: string; +} + +/** Device-local. `pinned` is in display order. */ +export interface UsagePreferences { + displayAs: UsageDisplayAs; + pinned: UsagePin[]; +} + +export const DEFAULT_USAGE_PREFERENCES: UsagePreferences = { displayAs: "used", pinned: [] }; + +export const UsagePreferencesSchema = z + .object({ + displayAs: z.enum(["used", "remaining"]).catch("used"), + pinned: z.array(z.object({ sourceId: z.string(), windowId: z.string() })).catch([]), + }) + .catch(DEFAULT_USAGE_PREFERENCES); + +function samePin(a: UsagePin, b: UsagePin): boolean { + return a.sourceId === b.sourceId && a.windowId === b.windowId; +} + +export function isUsagePinned(preferences: UsagePreferences, pin: UsagePin): boolean { + return preferences.pinned.some((pinned) => samePin(pinned, pin)); +} + +/** Unpins a pinned window; pins any other at the end, so the Usage item grows to the right. */ +export function toggleUsagePin(preferences: UsagePreferences, pin: UsagePin): UsagePreferences { + const pinned = isUsagePinned(preferences, pin) + ? preferences.pinned.filter((existing) => !samePin(existing, pin)) + : [...preferences.pinned, { sourceId: pin.sourceId, windowId: pin.windowId }]; + return { ...preferences, pinned }; +} + +export function setUsageDisplayAs( + preferences: UsagePreferences, + displayAs: UsageDisplayAs, +): UsagePreferences { + return { ...preferences, displayAs }; +} diff --git a/packages/app/src/usage/queries.ts b/packages/app/src/usage/queries.ts index a7a717b627..753fb17af5 100644 --- a/packages/app/src/usage/queries.ts +++ b/packages/app/src/usage/queries.ts @@ -1,7 +1,8 @@ +import { supportsUsageReports } from "@getpaseo/client/internal/daemon-client"; import { useCallback, useMemo } from "react"; import { useMutation, useQueryClient, type QueryClient } from "@tanstack/react-query"; import { useShallow } from "zustand/shallow"; -import { useFetchQueries, useFetchQuery } from "@/data/query"; +import { useFetchQuery } from "@/data/query"; import { getHostRuntimeStore, useHostRuntimeConnectionStatuses, @@ -11,13 +12,10 @@ import { import { useSessionStore, type SessionState } from "@/stores/session-store"; import { usageCopy } from "./copy"; import { - groupUsageByHost, replaceReport, - resolveUsagePill, resolveUsageRefresh, resolveUsageView, - type UsageHostGroup, - type UsagePill, + type UsageHost, type UsageQueryState, type UsageRefresh, } from "./model"; @@ -31,10 +29,6 @@ function usageReportsQueryKey(serverId: string) { return ["usage", "reports", serverId] as const; } -function agentUsageQueryKey(serverId: string, agentId: string, model: string | null) { - return ["usage", "agent", serverId, agentId, model] as const; -} - function requireClient(serverId: string) { const client = getHostRuntimeStore().getClient(serverId); if (!client) throw new Error(usageCopy.clientUnavailable); @@ -45,10 +39,6 @@ async function listReports(serverId: string, forceRefresh = false): Promise { await queryClient.fetchQuery({ queryKey: usageReportsQueryKey(serverId), - queryFn: async () => { - const reports = await listReports(serverId, true); - for (const report of reports) - queryClient.setQueryData(reportQueryKey(serverId, report.id), report); - return reports; - }, + queryFn: () => listReports(serverId, true), staleTime: 0, }); } @@ -92,12 +77,7 @@ export function useHostUsage(serverId: string): { view: UsageView; refresh: () = const isSupported = useSessionStore((state) => supportsUsage(state.sessions[serverId])); const query = useFetchQuery({ queryKey: usageReportsQueryKey(serverId), - queryFn: async () => { - const reports = await listReports(serverId); - for (const report of reports) - queryClient.setQueryData(reportQueryKey(serverId, report.id), report); - return reports; - }, + queryFn: () => listReports(serverId), enabled: isConnected && isSupported, dataShape: "list", staleTimeMs: REPORTS_STALE_TIME_MS, @@ -105,7 +85,9 @@ export function useHostUsage(serverId: string): { view: UsageView; refresh: () = const refresh = useCallback(() => { void refreshReports(queryClient, serverId).catch(() => undefined); }, [queryClient, serverId]); + const hostLabel = useHosts().find((host) => host.serverId === serverId)?.label ?? serverId; const view = resolveUsageView({ + hostLabel, isConnected, supportsUsage: isSupported, query: toQueryState(query), @@ -113,19 +95,15 @@ export function useHostUsage(serverId: string): { view: UsageView; refresh: () = return { view, refresh }; } -/** Usage reports for every connected host, grouped by host. */ -export function useUsageByHost(): { - groups: UsageHostGroup[]; - refresh: (serverId: string) => void; -} { - const queryClient = useQueryClient(); +/** Every host with whether it is connected and reports usage, in host order. */ +export function useUsageHosts(): UsageHost[] { const hosts = useHosts(); const serverIds = useMemo(() => hosts.map((host) => host.serverId), [hosts]); const connectionStatuses = useHostRuntimeConnectionStatuses(serverIds); const supportedServerIds = useSessionStore( useShallow((state) => serverIds.filter((serverId) => supportsUsage(state.sessions[serverId]))), ); - const usageHosts = useMemo( + return useMemo( () => hosts.map((host) => ({ serverId: host.serverId, @@ -135,70 +113,12 @@ export function useUsageByHost(): { })), [connectionStatuses, hosts, supportedServerIds], ); - const results = useFetchQueries( - usageHosts.map((host) => ({ - queryKey: usageReportsQueryKey(host.serverId), - queryFn: () => listReports(host.serverId), - enabled: host.isConnected && host.supportsUsage, - dataShape: "list", - staleTimeMs: REPORTS_STALE_TIME_MS, - })), - ); - const groups = groupUsageByHost( - usageHosts, - new Map(usageHosts.map((host, index) => [host.serverId, toQueryState(results[index])])), - ); - const refresh = useCallback( - (serverId: string) => { - void refreshReports(queryClient, serverId).catch(() => undefined); - }, - [queryClient], - ); - return { groups, refresh }; } /** - * The usage report for the account an agent is spending. The daemon resolves the - * report ID, which is re-resolved when the model changes (a new key) and when a - * turn completes (the query is paused while the agent runs, and its data is always - * stale, so resuming refetches). The report itself is read from the shared cache. - */ -export function useAgentUsage( - serverId: string, - agentId: string, -): { pill: UsagePill | null; entry: UsageReportEntry | null } { - const isConnected = useHostRuntimeIsConnected(serverId); - const isSupported = useSessionStore((state) => supportsUsage(state.sessions[serverId])); - const { model, isRunning } = useSessionStore( - useShallow((state) => { - const agent = state.sessions[serverId]?.agents?.get(agentId); - return { model: agent?.model ?? null, isRunning: agent?.status === "running" }; - }), - ); - const identity = useFetchQuery({ - queryKey: agentUsageQueryKey(serverId, agentId, model), - queryFn: async () => - (await requireClient(serverId).resolveAgentUsageReport({ agentId })).reportId, - enabled: isConnected && isSupported && !isRunning, - dataShape: "value", - staleTimeMs: 0, - }); - const reportId = identity.data ?? null; - const query = useFetchQuery({ - queryKey: reportQueryKey(serverId, reportId ?? "none"), - queryFn: () => getReport(serverId, reportId!), - enabled: isConnected && isSupported && reportId !== null, - dataShape: "value", - staleTimeMs: REPORTS_STALE_TIME_MS, - }); - const entry = isSupported ? (query.data ?? null) : null; - return { pill: resolveUsagePill({ supportsUsage: isSupported, entry }), entry }; -} - -/** - * Forces the source to fetch one report, and only that report. The result replaces - * the report wherever it is cached — its own entry and its host's list — so every - * surface showing it moves together; until then the previous report stays on screen. + * Forces the source to fetch one report, and only that report. The result replaces the report + * in its host's list, so every surface showing it moves together; until then the previous report + * stays on screen. */ export function useReportRefresh( serverId: string, @@ -208,7 +128,6 @@ export function useReportRefresh( const mutation = useMutation({ mutationFn: () => getReport(serverId, reportId, true), onSuccess: (report) => { - queryClient.setQueryData(reportQueryKey(serverId, reportId), report); queryClient.setQueryData(usageReportsQueryKey(serverId), (reports) => reports ? replaceReport(reports, reportId, report) : reports, ); diff --git a/packages/app/src/usage/sidebar-item.tsx b/packages/app/src/usage/sidebar-item.tsx new file mode 100644 index 0000000000..c4993b8b2e --- /dev/null +++ b/packages/app/src/usage/sidebar-item.tsx @@ -0,0 +1,242 @@ +import { router } from "expo-router"; +import { Gauge } from "lucide-react-native"; +import { useCallback, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Pressable, Text, View, type PressableStateCallbackType } from "react-native"; +import { StyleSheet } from "react-native-unistyles"; +import { SidebarHeaderRow } from "@/components/sidebar/sidebar-header-row"; +import { SidebarPopoverRoot, SidebarPopoverSurface } from "@/components/sidebar/sidebar-popover"; +import { useIsCompactFormFactor } from "@/constants/layout"; +import { builtinSidebarNavLabelKey } from "@/sidebar-nav/model"; +import { usePanelStore } from "@/stores/panel-store"; +import { buildUsageRoute } from "@/utils/host-routes"; +import { useHostUsageWithControls } from "./controls"; +import { useUsagePreferences, type UsageDisplay } from "./display"; +import { useUsageHostId, useUsageHostSelection } from "./hosts"; +import type { UsagePreferences } from "./preferences"; +import { useHostUsage } from "./queries"; +import { UsageSourceIcon } from "./source-icon"; +import { resolvePinnedUsage, type PinnedUsageWindow } from "./pinned"; +import type { UsageReportEntry } from "./types"; +import type { UsageHost } from "./model"; +import { UsageBody } from "./usage-section"; + +const NO_REPORTS: UsageReportEntry[] = []; +const NO_ITEMS: PinnedUsageWindow[] = []; + +/** + * The sidebar footer's usage entry: each pinned window's source icon and percent, or a plain + * "Usage" row while no pinned window has data. Pressing it opens the Usage screen; on compact + * layouts it opens the usage sheet instead. + */ +export function UsageSidebarItem() { + const { preferences, display } = useUsagePreferences(); + const serverId = useUsageHostId(); + // Without pins there is nothing to summarize, so the host is not asked for reports. + if (!serverId || preferences.pinned.length === 0) { + return ; + } + return ( + + ); +} + +function PinnedUsageItem({ + serverId, + preferences, + display, +}: { + serverId: string; + preferences: UsagePreferences; + display: UsageDisplay; +}) { + const { view } = useHostUsage(serverId); + const reports = view.kind === "ready" ? view.reports : NO_REPORTS; + const items = useMemo(() => resolvePinnedUsage(reports, preferences), [preferences, reports]); + return ; +} + +function useOpenUsageScreen(): () => void { + const isCompact = useIsCompactFormFactor(); + const showMobileAgent = usePanelStore((state) => state.showMobileAgent); + return useCallback(() => { + if (isCompact) showMobileAgent(); + router.push(buildUsageRoute()); + }, [isCompact, showMobileAgent]); +} + +function UsageEntry({ + serverId, + items, + display, +}: { + serverId: string | null; + items: readonly PinnedUsageWindow[]; + display: UsageDisplay; +}) { + const { t } = useTranslation(); + const label = t(builtinSidebarNavLabelKey("usage")); + const isCompact = useIsCompactFormFactor(); + const openUsageScreen = useOpenUsageScreen(); + const [open, setOpen] = useState(false); + // The sheet mounts on first open, so an unopened sidebar never asks the host for reports. + const [sheetMounted, setSheetMounted] = useState(false); + // Without a host there are no reports to show, so compact goes to the screen, which says so. + const usesSheet = isCompact && serverId !== null; + const handlePress = useCallback(() => { + if (!usesSheet) { + openUsageScreen(); + return; + } + setSheetMounted(true); + setOpen(true); + }, [openUsageScreen, usesSheet]); + + const trigger = + items.length > 0 ? ( + + ) : ( + + ); + if (!usesSheet) return trigger; + return ( + + {trigger} + {sheetMounted ? : null} + + ); +} + +/** + * The compact usage sheet: the Usage screen's host, reports with pins, and controls, the controls + * in its title row. + */ +function UsageSheet({ title, display }: { title: string; display: UsageDisplay }) { + const { serverId, connectedHosts, select } = useUsageHostSelection(); + if (!serverId) return null; + return ( + + ); +} + +function HostUsageSheet({ + title, + serverId, + hosts, + onSelectHost, + display, +}: { + title: string; + serverId: string; + hosts: UsageHost[]; + onSelectHost: (serverId: string) => void; + display: UsageDisplay; +}) { + const hostSelection = useMemo( + () => ({ hosts, serverId, onSelect: onSelectHost }), + [hosts, onSelectHost, serverId], + ); + const { view, refresh, controls } = useHostUsageWithControls(hostSelection, display); + return ( + + + + + + ); +} + +function pinnedUsageLabel(label: string, items: readonly PinnedUsageWindow[]): string { + return `${label}: ${items.map((item) => `${item.label} ${item.percentText}`).join(", ")}`; +} + +function triggerStyle({ hovered }: PressableStateCallbackType & { hovered?: boolean }) { + return hovered ? [styles.trigger, styles.triggerHovered] : styles.trigger; +} + +function PinnedUsageTrigger({ + label, + items, + onPress, +}: { + label: string; + items: readonly PinnedUsageWindow[]; + onPress: () => void; +}) { + return ( + + {items.map((item) => ( + + + + {item.percentText} + + + ))} + + ); +} + +const styles = StyleSheet.create((theme) => ({ + trigger: { + width: "100%", + flexDirection: "row", + flexWrap: "wrap", + alignItems: "center", + // A wrapping row packs its lines at the top; center them in the 28px row instead. + alignContent: "center", + columnGap: theme.spacing[3], + rowGap: theme.spacing[1], + // Same row geometry and leading rail as Add project and the footer icons. + minHeight: 28, + paddingVertical: theme.spacing[1], + paddingHorizontal: theme.spacing[1.5], + borderRadius: theme.borderRadius.lg, + }, + triggerHovered: { + backgroundColor: theme.colors.surfaceSidebarHover, + }, + item: { + flexDirection: "row", + alignItems: "center", + gap: theme.spacing[1], + }, + percent: { + color: theme.colors.foregroundMuted, + fontSize: theme.fontSize.sm, + fontVariant: ["tabular-nums"], + }, + sheetBody: { + padding: theme.spacing[3], + gap: theme.spacing[3], + }, +})); diff --git a/packages/app/src/usage/usage-screen.tsx b/packages/app/src/usage/usage-screen.tsx index 96006f7728..e1eaf0bb02 100644 --- a/packages/app/src/usage/usage-screen.tsx +++ b/packages/app/src/usage/usage-screen.tsx @@ -1,11 +1,14 @@ -import { useCallback } from "react"; import { useIsFocused } from "@react-navigation/native"; import { router } from "expo-router"; +import { useMemo, type ReactNode } from "react"; +import { View } from "react-native"; import { PageLayout } from "@/components/page-layout"; +import { useHostUsageWithControls } from "./controls"; import { usageCopy } from "./copy"; -import type { UsageHostGroup } from "./model"; -import { useUsageByHost } from "./queries"; -import { UsageMessage, UsageSection } from "./usage-section"; +import { useUsagePreferences } from "./display"; +import { useUsageHostSelection } from "./hosts"; +import type { UsageHost } from "./model"; +import { UsageBody, UsageMessage } from "./usage-section"; // The screen is reachable by URL, so there may be no history to go back to. function leaveUsage(): void { @@ -18,40 +21,52 @@ function leaveUsage(): void { export function UsageScreen() { const isFocused = useIsFocused(); + if (!isFocused) return {null}; + return ; +} + +function UsagePage({ actions, children }: { actions?: ReactNode; children: ReactNode }) { return ( - - {isFocused ? : null} + + {children} ); } -function UsageScreenContent() { - const { groups, refresh } = useUsageByHost(); +function FocusedUsageScreen() { + const { serverId, connectedHosts, select } = useUsageHostSelection(); + if (!serverId) { + return ( + + + + ); + } return ( - <> - {groups.length === 0 ? : null} - {groups.map((group) => ( - - ))} - + ); } -function HostUsageGroup({ - group, - onRefresh, +function HostUsage({ + serverId, + hosts, + onSelectHost, }: { - group: UsageHostGroup; - onRefresh: (serverId: string) => void; + serverId: string; + hosts: UsageHost[]; + onSelectHost: (serverId: string) => void; }) { - const handleRefresh = useCallback(() => onRefresh(group.serverId), [group.serverId, onRefresh]); + const { display } = useUsagePreferences(); + const hostSelection = useMemo( + () => ({ hosts, serverId, onSelect: onSelectHost }), + [hosts, onSelectHost, serverId], + ); + const { view, refresh, controls } = useHostUsageWithControls(hostSelection, display); return ( - + + + + + ); } diff --git a/packages/app/src/usage/usage-section.tsx b/packages/app/src/usage/usage-section.tsx index 846bc6d6f5..44b3ecc8ce 100644 --- a/packages/app/src/usage/usage-section.tsx +++ b/packages/app/src/usage/usage-section.tsx @@ -1,4 +1,3 @@ -import { RefreshCw } from "lucide-react-native"; import { useMemo } from "react"; import { Text, View } from "react-native"; import { StyleSheet } from "react-native-unistyles"; @@ -7,6 +6,8 @@ import { Button } from "@/components/ui/button"; import { settingsStyles } from "@/styles/settings"; import { SettingsSection } from "@/components/settings/headings/settings-section"; import { usageCopy } from "./copy"; +import type { UsageDisplay } from "./display"; +import { UsageControls } from "./controls"; import { UsageList } from "./list"; import type { UsageView } from "./types"; @@ -14,48 +15,39 @@ export function UsageSection({ serverId, title, view, + display, onRefresh, testID, }: { serverId: string; title: string; view: UsageView; + display: UsageDisplay; onRefresh: () => void; testID?: string; }) { - const busy = view.kind === "loading" || (view.kind === "ready" && view.isRefreshing); - - const refreshButton = useMemo( - () => - view.kind === "unavailable" ? null : ( - - ), - [busy, onRefresh, view.kind], + const trailing = useMemo( + () => , + [display, onRefresh, view], ); return ( - - + + ); } -function UsageBody({ +/** What a host's usage view shows: its reports, or why there are none. */ +export function UsageBody({ serverId, view, + display, onRefresh, }: { serverId: string; view: UsageView; + display: UsageDisplay; onRefresh: () => void; }) { if (view.kind === "unavailable") { @@ -80,7 +72,7 @@ function UsageBody({ return ; } - return ; + return ; } export function UsageMessage({ text }: { text: string }) { diff --git a/packages/app/src/usage/window-bar.tsx b/packages/app/src/usage/window-bar.tsx index 9627cb1571..ce6e43b5e6 100644 --- a/packages/app/src/usage/window-bar.tsx +++ b/packages/app/src/usage/window-bar.tsx @@ -1,8 +1,9 @@ import { useMemo } from "react"; -import { Text, View, type StyleProp, type ViewStyle } from "react-native"; +import { Pressable, Text, View, type StyleProp, type ViewStyle } from "react-native"; import { StyleSheet } from "react-native-unistyles"; -import { clampPct, formatPct, formatResetLabel } from "./format"; -import { usedPercent } from "./model"; +import { clampPct, formatDisplayPct, formatResetLabel } from "./format"; +import { displayPercent, usageWindowRowLabel, usedPercent } from "./model"; +import type { UsageDisplayAs } from "./preferences"; import { deriveTone } from "./tone"; import type { UsageTone, UsageWindow } from "./types"; @@ -19,11 +20,34 @@ function fillToneStyle(tone: UsageTone) { } } -export function UsageWindowBar({ window }: { window: UsageWindow }) { +// Pinned rows carry the pinned surface; hovering an unpinned row previews it at half strength, +// so a hover never reads as the selection. Pinned rows do not react to hover. +function highlightStyle(pinned: boolean, hovered: boolean) { + if (pinned) return styles.highlightPinned; + return hovered ? styles.highlightHovered : styles.highlightNone; +} + +export function UsageWindowBar({ + window, + displayAs, + pinned, + onTogglePin, + pinLabel, + pinTestID, +}: { + window: UsageWindow; + displayAs: UsageDisplayAs; + pinned: boolean; + onTogglePin: () => void; + /** What the row pins, naming the source and window: "Pin Claude Session". */ + pinLabel: string; + pinTestID: string; +}) { const usedPct = usedPercent(window); + const shownPct = displayPercent(window, displayAs); const tone = window.tone ?? deriveTone(usedPct); - const fillWidth = clampPct(usedPct ?? 0); + const fillWidth = clampPct(shownPct ?? 0); const fillStyle = useMemo>( () => [styles.fill, fillToneStyle(tone), { width: `${fillWidth}%` }], [fillWidth, tone], @@ -34,14 +58,59 @@ export function UsageWindowBar({ window }: { window: UsageWindow }) { ? `runs out ${formatResetLabel(window.runsOutAt)?.replace("resets ", "") ?? ""}`.trim() : formatResetLabel(window.resetsAt); + const value = shownPct != null ? formatDisplayPct(shownPct, displayAs) : "—"; + const accessibilityState = useMemo(() => ({ checked: pinned }), [pinned]); + + // The whole row pins the window to the sidebar Usage item. Pinned or not, it keeps the same + // padding so toggling only changes the background. return ( - + + {({ hovered }: { hovered?: boolean }) => ( + + )} + + ); +} + +function WindowRowContent({ + highlight, + label, + value, + trailing, + isAtRisk, + fillStyle, +}: { + highlight: StyleProp; + label: string; + value: string; + trailing: string | null | undefined; + isAtRisk: boolean; + fillStyle: StyleProp; +}) { + return ( + <> + - {window.label} + {label} - {usedPct != null ? formatPct(usedPct) : "—"} + {value} {trailing ? ( {` · ${trailing}`} ) : null} @@ -50,13 +119,36 @@ export function UsageWindowBar({ window }: { window: UsageWindow }) { - + ); } const styles = StyleSheet.create((theme) => ({ - container: { + row: { gap: 3, + // The highlight bleeds into the card padding so the label and bar stay on the card's rail. + marginHorizontal: -theme.spacing[2], + paddingHorizontal: theme.spacing[2], + paddingVertical: theme.spacing[1.5], + // Its own stacking context, so the highlight layer paints above the card and below the text. + zIndex: 0, + }, + highlightNone: { + display: "none", + }, + highlightPinned: { + ...StyleSheet.absoluteFillObject, + zIndex: -1, + borderRadius: theme.borderRadius.md, + backgroundColor: theme.colors.surface2, + }, + // The pinned surface at half strength: a separate layer, so the text keeps full opacity. + highlightHovered: { + ...StyleSheet.absoluteFillObject, + zIndex: -1, + borderRadius: theme.borderRadius.md, + backgroundColor: theme.colors.surface2, + opacity: theme.opacity[50], }, labelRow: { flexDirection: "row", @@ -65,7 +157,7 @@ const styles = StyleSheet.create((theme) => ({ gap: theme.spacing[2], }, label: { - flexShrink: 1, + flex: 1, color: theme.colors.foregroundMuted, fontSize: theme.fontSize.sm, }, diff --git a/packages/cli/src/commands/plugin/scaffold.test.ts b/packages/cli/src/commands/plugin/scaffold.test.ts index d4d8a682cf..df0c13295b 100644 --- a/packages/cli/src/commands/plugin/scaffold.test.ts +++ b/packages/cli/src/commands/plugin/scaffold.test.ts @@ -228,7 +228,7 @@ import { AgentPanel, contributeClient, Surface } from "./client/main"; import { inspect } from "./shared/inspect"; export default function contribute(client: PluginClientContext) { - client.addSurface("main", Surface); + client.addScreen({ id: "main", title: "Review", Component: Surface }); client.addWorkspacePanel({ id: "review", title: "Review", @@ -294,7 +294,9 @@ export default function contribute(client: PluginClientContext) { `, ); const check = typecheckPlugin(directory); - await expect(check).rejects.toThrow("has no exported member 'PluginComposerPillProps'"); + await expect(check).rejects.toThrow( + /has no exported member (named )?'PluginComposerPillProps'/, + ); await expect(check).rejects.toThrow("Property 'button' is missing"); await expect(check).rejects.toThrow("has no call signatures"); }, 20_000); diff --git a/packages/cli/src/commands/plugin/scaffold.ts b/packages/cli/src/commands/plugin/scaffold.ts index 2046b0cd8e..448aa956fa 100644 --- a/packages/cli/src/commands/plugin/scaffold.ts +++ b/packages/cli/src/commands/plugin/scaffold.ts @@ -20,17 +20,23 @@ const TSCONFIG = { include: ["**/*.ts", "**/*.tsx"], }; -const CLIENT_ENTRY = `import type { PluginClientContext } from "@getpaseo/plugin/client"; -import { GreetingSurface } from "./client/greeting"; +const CLIENT_ENTRY = `import type { PluginClientContext, PluginSidebarItemProps } from "@getpaseo/plugin/client"; +import { SidebarRow } from "@getpaseo/plugin/client/ui"; +import { GreetingScreen } from "./client/greeting"; + +function GreetingItem({ currentScreen, openScreen }: PluginSidebarItemProps) { + return ( + openScreen({ screenId: "greeting" })} + /> + ); +} export default function contribute(client: PluginClientContext) { - client.addSurface("greeting", GreetingSurface); - client.addSidebarItem({ - id: "greeting", - title: "Greeting", - icon: "MessageCircle", - surface: "greeting", - }); + client.addScreen({ id: "greeting", title: "Greeting", Component: GreetingScreen }); + client.addSidebarHeaderItem({ id: "greeting", title: "Greeting", Component: GreetingItem }); return () => {}; } `; @@ -63,7 +69,7 @@ export function createGreeting({ name }: RpcInput) { } `; -const CLIENT_GREETING = `import type { PluginSurfaceProps } from "@getpaseo/plugin/client"; +const CLIENT_GREETING = `import type { PluginScreenProps } from "@getpaseo/plugin/client"; import { useRpc } from "@getpaseo/plugin/client"; import { useMutation } from "@tanstack/react-query"; import { useMemo } from "react"; @@ -71,7 +77,7 @@ import { Pressable, Text, View } from "react-native"; import { greetingRpc } from "../shared/greeting"; import { openExternal } from "./web"; -export function GreetingSurface({ theme, layout }: PluginSurfaceProps) { +export function GreetingScreen({ theme, layout }: PluginScreenProps) { const createGreeting = useRpc(greetingRpc); const greeting = useMutation({ mutationFn: createGreeting }); const styles = useMemo( diff --git a/packages/client/src/daemon-client.test.ts b/packages/client/src/daemon-client.test.ts index dbe3ceef64..0f6d7f1e07 100644 --- a/packages/client/src/daemon-client.test.ts +++ b/packages/client/src/daemon-client.test.ts @@ -2,6 +2,7 @@ import { afterEach, expect, expectTypeOf, test, vi } from "vitest"; import { z } from "zod"; import { DaemonClient, + supportsUsageReports, type DaemonClientTrace, type CreateAgentRequestOptions, type DaemonTransport, @@ -6572,6 +6573,101 @@ test("sends provider.usage.list.request and resolves provider.usage.list.respons }); }); +test("maps released-host usage reports and filters report IDs", async () => { + const mock = createMockTransport(); + const client = new DaemonClient({ + url: "ws://test", + clientId: "clsk_unit_test", + logger: createMockLogger(), + reconnect: { enabled: false }, + transportFactory: () => mock.transport, + }); + clients.push(client); + const connected = client.connect(); + mock.triggerOpen({ features: { providerUsageList: true } }); + await connected; + const providers = [ + { + providerId: "claude", + displayName: "Claude", + status: "available", + planLabel: null, + fetchedAt: null, + windows: [{ id: "session", label: "Session", usedPct: 25 }], + error: null, + }, + { + providerId: "codex", + displayName: "Codex", + status: "error", + planLabel: "Pro", + fetchedAt: "2026-09-29T00:00:00.000Z", + windows: [], + balances: [], + details: [], + error: "unavailable", + }, + ]; + const reports = [ + { + id: "claude", + sourceId: "claude", + sourceLabel: "Claude", + account: {}, + fetchedAt: "2026-09-30T00:00:00.000Z", + report: { + status: "available", + planLabel: undefined, + windows: providers[0]!.windows, + balances: undefined, + details: undefined, + error: undefined, + }, + }, + { + id: "codex", + sourceId: "codex", + sourceLabel: "Codex", + account: {}, + fetchedAt: "2026-09-29T00:00:00.000Z", + report: { + status: "error", + planLabel: "Pro", + windows: [], + balances: [], + details: [], + error: "unavailable", + }, + }, + ]; + for (const reportIds of [undefined, ["codex", "missing"], []]) { + const result = client.listUsageReports({ + requestId: "legacy-usage", + reportIds, + forceRefresh: true, + }); + expect(parseSentFrame(mock.sent.at(-1))).toEqual({ + type: "provider.usage.list.request", + requestId: "legacy-usage", + }); + mock.triggerMessage( + wrapSessionMessage({ + type: "provider.usage.list.response", + payload: { + requestId: "legacy-usage", + fetchedAt: "2026-09-30T00:00:00.000Z", + providers, + }, + }), + ); + expect(await result).toStrictEqual({ + requestId: "legacy-usage", + reports: + reportIds === undefined ? reports : reports.filter((entry) => reportIds.includes(entry.id)), + }); + } +}); + test("sends close_items_request and resolves close_items_response", async () => { const logger = createMockLogger(); const mock = createMockTransport(); @@ -7100,3 +7196,72 @@ test("reviewed plugin updates gate before requests and preserve exact proposal d ]); } }); + +test.each([ + [undefined, false], + [{}, false], + [{ usageSources: false, providerUsageList: false }, false], + [{ usageSources: true }, true], + [{ providerUsageList: true }, true], + [{ usageSources: true, providerUsageList: true }, true], +] as const)("usage support for features %j is %s", (features, expected) => { + expect(supportsUsageReports(features)).toBe(expected); +}); + +test("uses modern usage RPC when both capabilities are advertised", async () => { + const mock = createMockTransport(); + const client = new DaemonClient({ + url: "ws://test", + clientId: "clsk_unit_test", + logger: createMockLogger(), + reconnect: { enabled: false }, + transportFactory: () => mock.transport, + }); + clients.push(client); + const connected = client.connect(); + mock.triggerOpen({ features: { usageSources: true, providerUsageList: true } }); + await connected; + const result = client.listUsageReports({ + requestId: "modern-usage", + reportIds: ["claude:work"], + forceRefresh: true, + }); + expect(parseSentFrame(mock.sent[0])).toEqual({ + type: "usage.list_reports.request", + requestId: "modern-usage", + reportIds: ["claude:work"], + forceRefresh: true, + }); + const payload = { + requestId: "modern-usage", + reports: [ + { + id: "claude:work", + sourceId: "claude", + sourceLabel: "Claude", + account: { label: "Work" }, + fetchedAt: "2026-09-30T00:00:00.000Z", + report: { status: "available", windows: [] }, + }, + ], + }; + mock.triggerMessage(wrapSessionMessage({ type: "usage.list_reports.response", payload })); + expect(await result).toStrictEqual(payload); +}); + +test("rejects usage requests when the host has neither capability", async () => { + const mock = createMockTransport(); + const client = new DaemonClient({ + url: "ws://test", + clientId: "clsk_unit_test", + logger: createMockLogger(), + reconnect: { enabled: false }, + transportFactory: () => mock.transport, + }); + clients.push(client); + const connected = client.connect(); + mock.triggerOpen({ features: {} }); + await connected; + await expect(client.listUsageReports()).rejects.toThrow("Update the host to see usage."); + expect(mock.sent).toEqual([]); +}); diff --git a/packages/client/src/daemon-client.ts b/packages/client/src/daemon-client.ts index 2fe706c8ae..ba1e09e7cb 100644 --- a/packages/client/src/daemon-client.ts +++ b/packages/client/src/daemon-client.ts @@ -95,7 +95,6 @@ import type { ProviderDiagnosticResponseMessage, ProviderUsageListResponseMessage, UsageListReportsResponseMessage, - AgentResolveUsageReportResponseMessage, DaemonGetStatusResponse, DaemonGetPairingOfferResponse, DaemonConfigReloadResponse, @@ -563,7 +562,6 @@ type RefreshProvidersSnapshotPayload = RefreshProvidersSnapshotResponseMessage[" type ProviderDiagnosticPayload = ProviderDiagnosticResponseMessage["payload"]; type ProviderUsageListPayload = ProviderUsageListResponseMessage["payload"]; type UsageListReportsPayload = UsageListReportsResponseMessage["payload"]; -type AgentResolveUsageReportPayload = AgentResolveUsageReportResponseMessage["payload"]; type DaemonStatusPayload = DaemonGetStatusResponse["payload"]; type DaemonPairingOfferPayload = DaemonGetPairingOfferResponse["payload"]; type DiagnosticsPayload = DiagnosticsResponse["payload"]; @@ -1188,6 +1186,11 @@ interface PingProbe { drivesLivenessFailure: boolean; } +export function supportsUsageReports(features: ServerInfoStatusPayload["features"]): boolean { + // COMPAT(providerUsageList): added in v0.1.98, remove after 2027-03-26. + return features?.usageSources === true || features?.providerUsageList === true; +} + export class DaemonClient { private readonly providerSnapshotUpdates = new ProviderSnapshotUpdates({ active: (message) => this.owned.owns(message), @@ -5260,6 +5263,37 @@ export class DaemonClient { forceRefresh?: boolean; reportIds?: string[]; }): Promise { + const features = this.getLastServerInfoMessage()?.features; + if (!supportsUsageReports(features)) { + throw new Error("Update the host to see usage."); + } + // COMPAT(providerUsageList): added in v0.1.98, remove after 2027-03-26. + if (features?.usageSources !== true) { + // Released hosts serve a five-minute cache and have no forceRefresh option. + const payload = await this.listProviderUsage({ requestId: options?.requestId }); + return { + requestId: payload.requestId, + reports: payload.providers + .filter( + (provider) => !options?.reportIds || options.reportIds.includes(provider.providerId), + ) + .map((provider) => ({ + id: provider.providerId, + sourceId: provider.providerId, + sourceLabel: provider.displayName, + account: {}, + fetchedAt: provider.fetchedAt ?? payload.fetchedAt, + report: { + status: provider.status, + windows: provider.windows, + balances: provider.balances ?? undefined, + details: provider.details ?? undefined, + planLabel: provider.planLabel ?? undefined, + error: provider.error ?? undefined, + }, + })), + }; + } return this.sendNamespacedCorrelatedSessionRequest({ requestId: options?.requestId, message: { @@ -5270,19 +5304,6 @@ export class DaemonClient { }); } - async resolveAgentUsageReport(options: { - agentId: string; - requestId?: string; - }): Promise { - return this.sendNamespacedCorrelatedSessionRequest({ - requestId: options.requestId, - message: { - type: "agent.resolve_usage_report.request", - agentId: options.agentId, - }, - }); - } - async listCommands(options: ListCommandsOptions): Promise; async listCommands(agentId: string, requestId?: string): Promise; async listCommands( diff --git a/packages/plugin/src/client/contracts.ts b/packages/plugin/src/client/contracts.ts index e1605e20f6..239b3580ee 100644 --- a/packages/plugin/src/client/contracts.ts +++ b/packages/plugin/src/client/contracts.ts @@ -47,8 +47,46 @@ interface PluginNavigableHostProps extends PluginHostProps { }; } +/** String keys and values: params travel in the screen's URL. */ +export type PluginScreenParams = Record; + +export interface PluginOpenScreenInput { + screenId: string; + params?: PluginScreenParams; +} + +export interface PluginScreenLocation { + screenId: string; + params: PluginScreenParams; +} + export interface PluginSurfaceProps extends PluginNavigableHostProps {} +export interface PluginScreenProps extends PluginSurfaceProps { + /** The params the screen was opened with; `{}` when none. */ + params: PluginScreenParams; +} + +export interface PluginPopoverProps extends PluginHostProps { + close(): void; + openScreen(input: PluginOpenScreenInput): void; +} + +export interface PluginSidebarItemProps extends PluginHostProps { + /** This plugin's screen open on this item's host, with its params, else null. */ + currentScreen: PluginScreenLocation | null; + openScreen(input: PluginOpenScreenInput): void; + /** Anchored to the item on wide layouts; a bottom sheet on compact layouts. */ + openPopover(Content: ComponentType): void; +} + +export interface PluginSidebarItemContribution { + id: string; + /** Settings row label, accessibility label, and default label for SidebarRow. */ + title: string; + Component: ComponentType; +} + export interface PluginIconProps { name: string; size?: number; @@ -84,10 +122,19 @@ export interface PluginClientOpenPanelOptions extends PluginOpenPanelOptions { agentId?: string; } -export interface PluginClientContext extends PluginCommandCapabilities { - addSettingsScreen(contribution: PluginSettingsScreenContribution): PluginCleanup; +// COMPAT(pluginSidebarAliases): added in v0.11.0, remove after 2027-03-29 +interface PluginClientContextAliases { + /** @deprecated Use `addScreen`. */ addSurface(id: string, Component: ComponentType): PluginCleanup; + /** @deprecated Use `addSidebarHeaderItem`. */ addSidebarItem(contribution: PluginSidebarContribution): PluginCleanup; +} + +export interface PluginClientContext extends PluginCommandCapabilities, PluginClientContextAliases { + addSettingsScreen(contribution: PluginSettingsScreenContribution): PluginCleanup; + addScreen(contribution: PluginScreenContribution): PluginCleanup; + addSidebarHeaderItem(contribution: PluginSidebarItemContribution): PluginCleanup; + addSidebarFooterItem(contribution: PluginSidebarItemContribution): PluginCleanup; addWorkspacePanel(contribution: PluginWorkspacePanelContribution): PluginCleanup; addCommandCenterItem(contribution: PluginCommandCenterItemContribution): PluginCleanup; addSlashCommand(contribution: PluginClientSlashCommandContribution): PluginCleanup; @@ -123,11 +170,21 @@ export interface PluginSettingsScreenContribution { Component: ComponentType; } +/** The screen header's title: fixed, or derived from the params the screen was opened with. */ +export type PluginScreenTitle = string | ((params: PluginScreenParams) => string); + +export interface PluginScreenContribution { + id: string; + title: PluginScreenTitle; + Component: ComponentType; +} + export interface PluginSurfaceContribution { id: string; Component: ComponentType; } +/** @deprecated Use `PluginSidebarItemContribution` with `addSidebarHeaderItem`. */ export interface PluginSidebarContribution { id: string; title: string; @@ -174,6 +231,8 @@ export interface PluginCommandCapabilities { contract: PluginRpcContract, input: ZodInput, ): Promise>; + openScreen(input: PluginOpenScreenInput): void; + /** @deprecated Use `openScreen`. */ openSurface(id: string): void; openSettings(id: string): void; } diff --git a/packages/plugin/src/client/index.ts b/packages/plugin/src/client/index.ts index b6f0e342f7..810462354f 100644 --- a/packages/plugin/src/client/index.ts +++ b/packages/plugin/src/client/index.ts @@ -1,6 +1,13 @@ export type { PluginHostProps, PluginSurfaceProps, + PluginScreenProps, + PluginPopoverProps, + PluginScreenParams, + PluginOpenScreenInput, + PluginScreenLocation, + PluginSidebarItemProps, + PluginSidebarItemContribution, PluginIconProps, PluginPanelLocation, PluginOpenPanelOptions, @@ -12,6 +19,8 @@ export type { PluginWorkspacePanelContribution, PluginSettingsScreenContribution, PluginSurfaceContribution, + PluginScreenContribution, + PluginScreenTitle, PluginSidebarContribution, PluginTimelineTransformerContribution, PluginTimelineItemProps, diff --git a/packages/plugin/src/client/ui.ts b/packages/plugin/src/client/ui.ts index 35abb0475d..6686d8292b 100644 --- a/packages/plugin/src/client/ui.ts +++ b/packages/plugin/src/client/ui.ts @@ -63,3 +63,24 @@ export interface ExternalLinkProps { onError?: (error: unknown) => void; } export declare const ExternalLink: ComponentType; + +export type SidebarIcon = string | ComponentType<{ size: number; color: string }>; +export interface SidebarRowProps { + /** + * Tells rows of one item apart when the item renders several: "bot-2". Unique within the item. + * Omit it when the item renders one row. + */ + id?: string; + /** A Lucide icon name or a component. */ + icon?: SidebarIcon; + /** Defaults to the item's registered title. */ + label?: string; + onPress(): void; + active?: boolean; + /** Right slot. Renders beside the row's pressable, so a button here presses on its own. */ + trailing?: ReactNode; +} +/** A sidebar navigation row. Render it from a sidebar item's `Component`. */ +export declare const SidebarRow: ComponentType; +/** A line between groups of rows. Render it from a sidebar item's `Component`. */ +export declare const SidebarSeparator: ComponentType; diff --git a/packages/plugin/src/server/provider.ts b/packages/plugin/src/server/provider.ts index a72477d19d..c65a814753 100644 --- a/packages/plugin/src/server/provider.ts +++ b/packages/plugin/src/server/provider.ts @@ -18,7 +18,6 @@ export const PROVIDER_CAPABILITIES = [ "session.revert.files", "session.subsession", "session.unarchive", - "session.usage_reference", "permission", "permission.tool_policy", "timeline.plugin", @@ -228,7 +227,6 @@ export type ProviderInput = } | { type: "session.prompt"; sessionId: string; prompt: ProviderPrompt } | { type: "session.interrupt"; requestId: string; sessionId: string } - | { type: "session.usage_reference"; requestId: string; sessionId: string } | { type: "session.permission"; sessionId: string; @@ -528,11 +526,6 @@ export type ProviderEvent = | { type: "catalog"; requestId: string; catalog: ProviderCatalog } | { type: "sessions"; requestId: string; sessions: ProviderSessionSummary[] } | { type: "request.completed"; requestId: string } - | { - type: "usage_reference"; - requestId: string; - reference: { source: string; input: JsonValue } | null; - } | { type: "request.failed"; requestId: string; error: ProviderError } | { type: "session.opened"; @@ -616,8 +609,6 @@ export function requiredProviderCapabilities(input: ProviderInput): readonly Pro case "session.interrupt": case "session.close": return []; - case "session.usage_reference": - return ["session.usage_reference"]; case "sessions": return ["session.list"]; case "session.open": { @@ -896,13 +887,6 @@ export const ProviderInputSchema: z.ZodType = z.discriminatedUnio z .object({ type: z.literal("session.interrupt"), requestId: idSchema, sessionId: idSchema }) .strict(), - z - .object({ - type: z.literal("session.usage_reference"), - requestId: idSchema, - sessionId: idSchema, - }) - .strict(), z .object({ type: z.literal("session.permission"), @@ -1288,13 +1272,6 @@ export const ProviderEventSchema: z.ZodType = z.discriminatedUnio }) .strip(), z.object({ type: z.literal("request.completed"), requestId: idSchema }).strip(), - z - .object({ - type: z.literal("usage_reference"), - requestId: idSchema, - reference: z.object({ source: idSchema, input: z.json() }).nullable(), - }) - .strip(), z .object({ type: z.literal("request.failed"), requestId: idSchema, error: providerErrorSchema }) .strip(), diff --git a/packages/plugin/src/server/usage.test.ts b/packages/plugin/src/server/usage.test.ts index 0d1dd04ac5..1a8c5de939 100644 --- a/packages/plugin/src/server/usage.test.ts +++ b/packages/plugin/src/server/usage.test.ts @@ -17,14 +17,13 @@ test("shared usage tones preserve the app threshold contract", () => { expect([null, 0, 0.01].map(balanceToneFromRemaining)).toEqual(["default", "danger", "ok"]); }); -test("shared windows retain percentage, reset time, and optional headline", () => { - expect(windowFromUsedPct({ id: "a", label: "A", utilizationPct: 30, headline: true })).toEqual({ +test("shared windows retain percentage and reset time", () => { + expect(windowFromUsedPct({ id: "a", label: "A", utilizationPct: 30 })).toEqual({ id: "a", label: "A", usedPct: 30, remainingPct: 70, resetsAt: null, - headline: true, }); expect(windowFromUsedPct({ id: "b", label: "B", utilizationPct: null })).toEqual({ id: "b", diff --git a/packages/plugin/src/server/usage.ts b/packages/plugin/src/server/usage.ts index 3e34e7acf1..ed49db0f1b 100644 --- a/packages/plugin/src/server/usage.ts +++ b/packages/plugin/src/server/usage.ts @@ -11,7 +11,6 @@ export interface UsageWindow { runsOutAt?: string | null; shortfallPct?: number | null; tone?: "default" | "ok" | "warning" | "danger"; - headline?: boolean; } export interface UsageBalance { @@ -49,7 +48,7 @@ export interface UsageSourceRegistration { /** Stable account identity, resolved without fetching usage. */ identify(input: unknown): Promise<{ key: string; label?: string } | null>; fetch(input: unknown): Promise; - discover?(): Promise; + discover(): Promise; } export function windowFromUsedPct(input: { @@ -58,7 +57,6 @@ export function windowFromUsedPct(input: { utilizationPct: number | null | undefined; resetsAt?: string | null; tone?: UsageWindow["tone"]; - headline?: boolean; }): UsageWindow { const usedPct = typeof input.utilizationPct === "number" ? input.utilizationPct : null; const window: UsageWindow = { @@ -69,7 +67,6 @@ export function windowFromUsedPct(input: { resetsAt: input.resetsAt ?? null, }; if (input.tone) window.tone = input.tone; - if (input.headline) window.headline = true; return window; } diff --git a/packages/protocol/src/messages.ts b/packages/protocol/src/messages.ts index 589137d720..6549ca3aa6 100644 --- a/packages/protocol/src/messages.ts +++ b/packages/protocol/src/messages.ts @@ -1781,11 +1781,6 @@ export const UsageListReportsRequestMessageSchema = z.object({ reportIds: z.array(z.string()).optional(), forceRefresh: z.boolean().optional(), }); -export const AgentResolveUsageReportRequestMessageSchema = z.object({ - type: z.literal("agent.resolve_usage_report.request"), - requestId: z.string(), - agentId: z.string(), -}); export const ResumeAgentRequestMessageSchema = z.object({ type: z.literal("resume_agent_request"), @@ -3246,7 +3241,6 @@ export const SessionInboundMessageSchema = z.discriminatedUnion("type", [ ProviderDiagnosticRequestMessageSchema, ProviderUsageListRequestMessageSchema, UsageListReportsRequestMessageSchema, - AgentResolveUsageReportRequestMessageSchema, ResumeAgentRequestMessageSchema, ImportAgentRequestMessageSchema, RefreshAgentRequestMessageSchema, @@ -6247,7 +6241,7 @@ export const ProviderUsageListResponseMessageSchema = z.object({ export const UsageReportSchema = z.object({ status: ProviderUsageStatusSchema, planLabel: z.string().optional(), - windows: z.array(ProviderUsageWindowSchema.extend({ headline: z.boolean().optional() })), + windows: z.array(ProviderUsageWindowSchema), balances: z.array(ProviderUsageBalanceSchema).optional(), details: z.array(ProviderUsageDetailSchema).optional(), error: z.string().optional(), @@ -6265,10 +6259,6 @@ export const UsageListReportsResponseMessageSchema = z.object({ type: z.literal("usage.list_reports.response"), payload: z.object({ requestId: z.string(), reports: z.array(UsageReportEntrySchema) }), }); -export const AgentResolveUsageReportResponseMessageSchema = z.object({ - type: z.literal("agent.resolve_usage_report.response"), - payload: z.object({ requestId: z.string(), reportId: z.string().nullable() }), -}); const AgentSlashCommandSchema = z.object({ name: z.string(), @@ -6956,7 +6946,6 @@ export const SessionOutboundMessageSchema = z.discriminatedUnion("type", [ ProviderDiagnosticResponseMessageSchema, ProviderUsageListResponseMessageSchema, UsageListReportsResponseMessageSchema, - AgentResolveUsageReportResponseMessageSchema, ListCommandsResponseSchema, ListTerminalsResponseSchema, TerminalsChangedSchema, @@ -7138,9 +7127,6 @@ export type ProviderUsageTone = z.infer; export type UsageReport = z.infer; export type UsageReportEntry = z.infer; export type UsageListReportsResponseMessage = z.infer; -export type AgentResolveUsageReportResponseMessage = z.infer< - typeof AgentResolveUsageReportResponseMessageSchema ->; export type ProviderUsageStatus = z.infer; export type ProviderUsage = z.infer; export type ProviderUsageWindow = z.infer; diff --git a/packages/server/src/server/agent/agent-sdk-types.ts b/packages/server/src/server/agent/agent-sdk-types.ts index effd5157c1..f2b42525e9 100644 --- a/packages/server/src/server/agent/agent-sdk-types.ts +++ b/packages/server/src/server/agent/agent-sdk-types.ts @@ -646,11 +646,6 @@ export interface AgentPermissionResult { followUpPrompt?: AgentPromptInput; } -export interface UsageReference { - source: string; - input: JsonValue; -} - export interface AgentSession { readonly provider: AgentProvider; readonly id: string | null; @@ -659,8 +654,6 @@ export interface AgentSession { /** New provider-owned rows to commit on registration. streamHistory must also * replay them at their original timestamps; restored sessions omit old rows. */ readonly initialTimeline?: ImportedTimelineEntry[]; - /** Resolved at fetch time because model and credentials may change during a session. */ - getUsageReference?(): Promise; run(prompt: AgentPromptInput, options?: AgentRunOptions): Promise; startTurn(prompt: AgentPromptInput, options?: AgentRunOptions): Promise<{ turnId: string }>; steerActiveTurn?(prompt: AgentPromptInput, options: SteerActiveTurnOptions): Promise; diff --git a/packages/server/src/server/agent/plugin-provider.test.ts b/packages/server/src/server/agent/plugin-provider.test.ts index 8774b8784c..c18f30a108 100644 --- a/packages/server/src/server/agent/plugin-provider.test.ts +++ b/packages/server/src/server/agent/plugin-provider.test.ts @@ -396,24 +396,6 @@ describe("PluginAgentClientRegistry", () => { await registry.shutdown(); }); - test("returns no usage reference when a plugin provider lacks the capability", async () => { - const harness = createProviderHarness(); - const registry = new PluginAgentClientRegistry(createTestLogger()); - registry.replace([harness.registration]); - const client = registry.clients()[harness.registration.id]!; - const session = await client.createSession({ - provider: harness.registration.id, - cwd: "/workspace", - }); - try { - expect(await session.getUsageReference?.()).toBeNull(); - expect(harness.inputs.some((input) => input.type === "session.usage_reference")).toBe(false); - } finally { - await session.close(); - registry.replace([]); - } - }); - test("terminalizes an active turn exactly once when its plugin provider is removed", async () => { const harness = createProviderHarness({ completeTurn: false }); const registry = new PluginAgentClientRegistry(createTestLogger()); diff --git a/packages/server/src/server/agent/plugin-provider.ts b/packages/server/src/server/agent/plugin-provider.ts index e6712d3c3b..1c26066fa8 100644 --- a/packages/server/src/server/agent/plugin-provider.ts +++ b/packages/server/src/server/agent/plugin-provider.ts @@ -51,7 +51,6 @@ import type { ProviderRefreshContext, SteerActiveTurnOptions, SteerResult, - UsageReference, } from "./agent-sdk-types.js"; import { isDefaultAgentCreateConfigUnattended, @@ -108,15 +107,9 @@ function providerError(error: ProviderError): Error { function isProviderRequestReply( event: ProviderEvent, -): event is Extract< - ProviderEvent, - { type: "request.completed" | "catalog" | "sessions" | "usage_reference" } -> { +): event is Extract { return ( - event.type === "request.completed" || - event.type === "catalog" || - event.type === "sessions" || - event.type === "usage_reference" + event.type === "request.completed" || event.type === "catalog" || event.type === "sessions" ); } @@ -526,10 +519,7 @@ class ProviderRuntime { } private finishRequest( - event: Extract< - ProviderEvent, - { type: "request.completed" | "catalog" | "sessions" | "usage_reference" } - >, + event: Extract, ): void { const request = this.requests.get(event.requestId); if (!request) return; @@ -569,17 +559,6 @@ class ProviderRuntimeSession { return this.capabilities; } - async getUsageReference(): Promise { - if (!this.capabilities.includes("session.usage_reference")) return null; - const event = await this.runtime.complete({ - type: "session.usage_reference", - requestId: randomUUID(), - sessionId: this.providerSessionId, - }); - if (event.type !== "usage_reference") throw new Error("Invalid usage reference response"); - return event.reference; - } - onEvent(listener: (event: ProviderEvent) => void): () => void { this.listeners.add(listener); return () => this.listeners.delete(listener); @@ -1096,10 +1075,6 @@ class PluginAgentSession implements AgentSession { return agentCapabilities(this.bridge.negotiatedCapabilities); } - getUsageReference(): Promise { - return this.bridge.getUsageReference(); - } - get features(): AgentFeature[] { return this.bridge.config.settings.map((setting) => setting.type === "toggle" diff --git a/packages/server/src/server/agent/provider-registry.ts b/packages/server/src/server/agent/provider-registry.ts index 50ad1ae94d..569e842125 100644 --- a/packages/server/src/server/agent/provider-registry.ts +++ b/packages/server/src/server/agent/provider-registry.ts @@ -458,7 +458,6 @@ export function wrapSessionProvider(provider: AgentProvider, inner: AgentSession } }, getRuntimeInfo: async () => mapRuntimeInfo(provider, await inner.getRuntimeInfo()), - getUsageReference: inner.getUsageReference?.bind(inner), getAvailableModes: () => inner.getAvailableModes(), getCurrentMode: () => inner.getCurrentMode(), setMode: (modeId) => inner.setMode(modeId), diff --git a/packages/server/src/server/agent/providers/acp-agent.test.ts b/packages/server/src/server/agent/providers/acp-agent.test.ts index 0394900d3b..9d106a2c1a 100644 --- a/packages/server/src/server/agent/providers/acp-agent.test.ts +++ b/packages/server/src/server/agent/providers/acp-agent.test.ts @@ -222,13 +222,6 @@ function createSessionWithConfig( ); } -test("ACP usage reference uses the provider ID", async () => { - for (const provider of ["copilot", "cursor", "kimi", "custom-source"]) { - const session = createSessionWithConfig({ provider }); - expect(await session.getUsageReference()).toEqual({ source: provider, input: {} }); - } -}); - function createKiroSession( options: { waitForInitialCommands?: boolean; initialCommandsWaitTimeoutMs?: number } = {}, logger: ReturnType = createTestLogger(), diff --git a/packages/server/src/server/agent/providers/acp-agent.ts b/packages/server/src/server/agent/providers/acp-agent.ts index 019725ce3f..2d47bb3b6b 100644 --- a/packages/server/src/server/agent/providers/acp-agent.ts +++ b/packages/server/src/server/agent/providers/acp-agent.ts @@ -1633,10 +1633,6 @@ export class ACPAgentSession implements AgentSession, ACPClient { readonly provider: string; readonly capabilities: AgentCapabilityFlags; - async getUsageReference() { - return { source: this.provider, input: {} }; - } - private readonly logger: Logger; private readonly runtimeSettings?: ProviderRuntimeSettings; private readonly defaultCommand: [string, ...string[]]; diff --git a/packages/server/src/server/agent/providers/claude/agent.test.ts b/packages/server/src/server/agent/providers/claude/agent.test.ts index 3e91b9d639..dbd10aea4d 100644 --- a/packages/server/src/server/agent/providers/claude/agent.test.ts +++ b/packages/server/src/server/agent/providers/claude/agent.test.ts @@ -25,7 +25,6 @@ import type { } from "../../agent-sdk-types.js"; import type { AgentAttachment } from "@getpaseo/protocol/messages"; import { buildAgentPrompt, renderPromptAttachmentAsText } from "../../prompt-attachments.js"; -import { buildProviderRegistry } from "../../provider-registry.js"; interface TestClaudeSession { translateMessageToEvents(message: SDKMessage): AgentStreamEvent[]; @@ -530,56 +529,6 @@ describe("ClaudeAgentClient.fetchCatalog", () => { describe("ClaudeAgentClient binary resolution", () => { const logger = createTestLogger(); - test("Claude usage reference follows CLAUDE_CONFIG_DIR and excludes API overrides", async () => { - const client = new ClaudeAgentClient({ logger, resolveBinary: async () => "/test/claude/bin" }); - const session = await client.createSession( - { provider: "claude", cwd: process.cwd() }, - { env: { CLAUDE_CONFIG_DIR: "/accounts/second" } }, - ); - expect(await session.getUsageReference?.()).toEqual({ - source: "claude", - input: { configDir: "/accounts/second" }, - }); - await session.close(); - }); - - test.each(["ANTHROPIC_BASE_URL", "ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN"])( - "Claude usage reference is null with %s override", - async (name) => { - const client = new ClaudeAgentClient({ - logger, - resolveBinary: async () => "/test/claude/bin", - }); - const session = await client.createSession( - { provider: "claude", cwd: process.cwd() }, - { env: { [name]: "override" } }, - ); - expect(await session.getUsageReference?.()).toBeNull(); - await session.close(); - }, - ); - - test("Claude custom alias keeps its CLAUDE_CONFIG_DIR usage reference", async () => { - const registry = buildProviderRegistry(logger, { - providerOverrides: { - "work-claude": { - extends: "claude", - label: "Work Claude", - env: { CLAUDE_CONFIG_DIR: "/accounts/work" }, - }, - }, - }); - const session = await registry["work-claude"].createClient(logger).createSession({ - provider: "work-claude", - cwd: process.cwd(), - }); - expect(await session.getUsageReference?.()).toEqual({ - source: "claude", - input: { configDir: "/accounts/work" }, - }); - await session.close(); - }); - test("resolves the installed Claude Code version", async () => { await expect(resolveClaudeCodeVersion()).resolves.toMatch(/^\d+\.\d+\.\d+$/); }); diff --git a/packages/server/src/server/agent/providers/claude/agent.ts b/packages/server/src/server/agent/providers/claude/agent.ts index 7eecf56c96..dd79b57a25 100644 --- a/packages/server/src/server/agent/providers/claude/agent.ts +++ b/packages/server/src/server/agent/providers/claude/agent.ts @@ -3,7 +3,6 @@ import { randomUUID } from "node:crypto"; import fs from "node:fs"; import { promises } from "node:fs"; import path from "node:path"; -import { homedir } from "node:os"; import { type AgentDefinition, type CanUseTool, @@ -2042,15 +2041,6 @@ class ClaudeAgentSession implements AgentSession { readonly provider = "claude" as const; readonly capabilities = CLAUDE_CAPABILITIES; - async getUsageReference() { - const env = this.buildSdkEnv(); - if (env.ANTHROPIC_BASE_URL || env.ANTHROPIC_API_KEY || env.ANTHROPIC_AUTH_TOKEN) return null; - return { - source: "claude", - input: { configDir: env.CLAUDE_CONFIG_DIR || path.join(env.HOME || homedir(), ".claude") }, - }; - } - private readonly config: ClaudeAgentConfig; private readonly launchEnv?: Record; private readonly agentId?: string; diff --git a/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts b/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts index e0e7fa0379..bb7d56d07a 100644 --- a/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts +++ b/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts @@ -171,44 +171,6 @@ function createSession( return session; } -test("Codex usage reference follows CODEX_HOME and excludes custom base URLs", async () => { - const session = new CodexAppServerAgentSession( - createConfig(), - null, - createTestLogger(), - () => { - throw new Error("unused"); - }, - {}, - false, - false, - false, - undefined, - "interactive", - { CODEX_HOME: "/accounts/second" }, - ); - expect(await session.getUsageReference()).toEqual({ - source: "codex", - input: { codexHome: "/accounts/second" }, - }); - const custom = new CodexAppServerAgentSession( - createConfig(), - null, - createTestLogger(), - () => { - throw new Error("unused"); - }, - {}, - false, - false, - false, - undefined, - "interactive", - { CODEX_HOME: "/accounts/second", OPENAI_BASE_URL: "https://example.test" }, - ); - expect(await custom.getUsageReference()).toBeNull(); -}); - function createProviderWithFakeAppServer( appServer: FakeCodexAppServer, options?: { diff --git a/packages/server/src/server/agent/providers/codex-app-server-agent.ts b/packages/server/src/server/agent/providers/codex-app-server-agent.ts index cee50fbf4c..119be789b9 100644 --- a/packages/server/src/server/agent/providers/codex-app-server-agent.ts +++ b/packages/server/src/server/agent/providers/codex-app-server-agent.ts @@ -3521,7 +3521,6 @@ export class CodexAppServerAgentSession implements AgentSession { private readonly autoReviewEnabled: boolean = false, private readonly agentId?: string, private readonly initialResumePurpose: "interactive" | "history" = "interactive", - private readonly usageEnv: NodeJS.ProcessEnv = process.env, ) { this.logger = logger.child({ module: "agent", @@ -3549,17 +3548,6 @@ export class CodexAppServerAgentSession implements AgentSession { } } - async getUsageReference() { - if (this.usageEnv.OPENAI_BASE_URL) return null; - return { - source: "codex", - input: { - codexHome: - this.usageEnv.CODEX_HOME || path.join(this.usageEnv.HOME || os.homedir(), ".codex"), - }, - }; - } - get id(): string | null { return this.currentThreadId; } @@ -7262,7 +7250,6 @@ export class CodexAppServerAgentClient implements AgentClient { autoReviewEnabled, launchContext?.agentId, "interactive", - buildCodexAppServerEnv(this.runtimeSettings, launchContext?.env), ); await session.connect(); return session; @@ -7295,7 +7282,6 @@ export class CodexAppServerAgentClient implements AgentClient { autoReviewEnabled, launchContext?.agentId, options?.purpose ?? "interactive", - buildCodexAppServerEnv(this.runtimeSettings, launchContext?.env), ); await session.connect(); return session; diff --git a/packages/server/src/server/agent/providers/omp/agent.test.ts b/packages/server/src/server/agent/providers/omp/agent.test.ts index 6858ed216d..90b82cff4b 100644 --- a/packages/server/src/server/agent/providers/omp/agent.test.ts +++ b/packages/server/src/server/agent/providers/omp/agent.test.ts @@ -1,10 +1,8 @@ import { describe, expect, test } from "vitest"; import { setImmediate as waitForImmediate } from "node:timers/promises"; -import { mkdtempSync, mkdirSync, rmSync } from "node:fs"; import { mkdtemp, mkdir, writeFile } from "node:fs/promises"; -import { createRequire } from "node:module"; import { tmpdir } from "node:os"; -import path, { join } from "node:path"; +import path from "node:path"; import type { AgentStreamEvent } from "../../agent-sdk-types.js"; import type { PaseoToolCatalog } from "../../tools/types.js"; @@ -172,79 +170,6 @@ function createToolCatalog(): PaseoToolCatalog { } describe("OMP agent client and session", () => { - test("usage reference reads current get_state provider and account on each call", async () => { - const home = mkdtempSync(join(tmpdir(), "paseo-omp-reference-")); - const agentDir = join(home, "agent"); - mkdirSync(agentDir); - // Upstream: packages/ai/src/auth/sqlite-credential-store.ts:576-581,711-724. - const { DatabaseSync } = createRequire(import.meta.url)("node:sqlite") as { - DatabaseSync: new (path: string) => { - exec(sql: string): void; - prepare(sql: string): { run(...args: unknown[]): void }; - close(): void; - }; - }; - const db = new DatabaseSync(join(agentDir, "agent.db")); - db.exec(` - CREATE TABLE cache (key TEXT PRIMARY KEY, value TEXT NOT NULL, expires_at INTEGER NOT NULL); - CREATE INDEX idx_cache_expires ON cache(expires_at); - CREATE TABLE auth_credentials ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - provider TEXT NOT NULL, - credential_type TEXT NOT NULL, - data TEXT NOT NULL, - disabled_cause TEXT DEFAULT NULL, - identity_key TEXT DEFAULT NULL, - created_at INTEGER NOT NULL DEFAULT (CAST(strftime('%s','now') AS INTEGER)), - updated_at INTEGER NOT NULL DEFAULT (CAST(strftime('%s','now') AS INTEGER)) - ); - `); - const insert = db.prepare( - "INSERT INTO auth_credentials (provider, credential_type, data) VALUES (?, 'oauth', ?)", - ); - insert.run( - "openai-codex", - JSON.stringify({ - access: "codex-fixture", - accountId: "codex-account", - expires: Date.now() + 60_000, - }), - ); - insert.run( - "anthropic", - JSON.stringify({ - access: "claude-fixture", - accountId: "claude-account", - expires: Date.now() + 60_000, - }), - ); - db.close(); - try { - const omp = new OmpHarness(); - await omp.start({}, undefined, { - OMP_PROFILE: "", - PI_CODING_AGENT_DIR: agentDir, - XDG_DATA_HOME: "", - }); - const runtime = omp.runtime(); - runtime.state = { ...runtime.state, model: { provider: "openai-codex", id: "codex-model" } }; - const requestsBefore = runtime.getStateRequestCount; - expect(await omp.getUsageReference()).toEqual({ - source: "codex", - input: { accessToken: "codex-fixture", accountId: "codex-account" }, - }); - expect(runtime.getStateRequestCount).toBe(requestsBefore + 1); - runtime.state = { ...runtime.state, model: { provider: "anthropic", id: "claude-model" } }; - expect(await omp.getUsageReference()).toEqual({ - source: "claude", - input: { accessToken: "claude-fixture" }, - }); - expect(runtime.getStateRequestCount).toBe(requestsBefore + 2); - await omp.close(); - } finally { - rmSync(home, { recursive: true, force: true }); - } - }); test("owns launch configuration and registers native host tools", async () => { const omp = new OmpHarness(); await omp.start({ modeId: "ask" }, createToolCatalog()); diff --git a/packages/server/src/server/agent/providers/omp/agent.ts b/packages/server/src/server/agent/providers/omp/agent.ts index 8e931b55fd..4e700df275 100644 --- a/packages/server/src/server/agent/providers/omp/agent.ts +++ b/packages/server/src/server/agent/providers/omp/agent.ts @@ -115,7 +115,6 @@ import { mapOmpRpcUiPermissionRequest, } from "./rpc-ui-permission-mapper.js"; import { DEFAULT_OMP_THINKING_LEVEL, mapOmpModel } from "./map-omp-model.js"; -import { resolveOmpUsageReference } from "./usage-reference.js"; const OMP_PROVIDER = "omp"; const OMP_CORE_CAPABILITIES: AgentCapabilityFlags = { @@ -201,7 +200,6 @@ interface OmpAgentSessionOptions { noTurnScheduler?: OmpNoTurnScheduler; usagePollScheduler?: OmpUsagePollScheduler; providerIdleDeadlineMs?: number; - usageEnv?: NodeJS.ProcessEnv; /** * When false (resumed sessions), replayed session events are dropped until * the first prompt or agent_start so history is not re-emitted as live @@ -735,7 +733,6 @@ export class OmpAgentSession implements AgentSession { this.state = options.initialState; this.currentModeId = options.currentModeId ?? null; this.logger = options.logger; - this.usageEnv = options.usageEnv ?? process.env; this.live = options.live ?? true; this.providerIdleScheduler = options.providerIdleScheduler ?? createOmpProviderIdleScheduler(); this.providerIdleDeadlineMs = options.providerIdleDeadlineMs ?? OMP_PROVIDER_IDLE_DEADLINE_MS; @@ -785,12 +782,6 @@ export class OmpAgentSession implements AgentSession { private readonly restartRuntime: OmpAgentSessionOptions["restartRuntime"]; private readonly config: AgentSessionConfig; private readonly logger: Logger; - private readonly usageEnv: NodeJS.ProcessEnv; - - async getUsageReference() { - const state = await this.runtimeSession.getState(); - return resolveOmpUsageReference(state.sessionId, state.model?.provider ?? "", this.usageEnv); - } get id(): string | null { return this.state.sessionId; @@ -2324,7 +2315,6 @@ export class OmpAgentClient implements AgentClient { noTurnScheduler: this.noTurnScheduler, usagePollScheduler: this.usagePollScheduler, providerIdleDeadlineMs: this.providerIdleDeadlineMs, - usageEnv: { ...process.env, ...this.runtimeSettings?.env, ...launchContext?.env }, }); } catch (error) { await hostTools?.close(); @@ -2391,7 +2381,6 @@ export class OmpAgentClient implements AgentClient { noTurnScheduler: this.noTurnScheduler, usagePollScheduler: this.usagePollScheduler, providerIdleDeadlineMs: this.providerIdleDeadlineMs, - usageEnv: { ...process.env, ...this.runtimeSettings?.env, ...launchContext?.env }, live: false, }); } catch (error) { diff --git a/packages/server/src/server/agent/providers/omp/test-utils/omp-harness.ts b/packages/server/src/server/agent/providers/omp/test-utils/omp-harness.ts index 5c53dd3e4f..b292a9b32f 100644 --- a/packages/server/src/server/agent/providers/omp/test-utils/omp-harness.ts +++ b/packages/server/src/server/agent/providers/omp/test-utils/omp-harness.ts @@ -540,10 +540,6 @@ export class OmpHarness { return this.omp.latestSession(); } - async getUsageReference() { - return this.requireSession().getUsageReference(); - } - runningToolCallIds(): string[] { const statusByCall = new Map(); for (const item of this.timeline()) { diff --git a/packages/server/src/server/agent/providers/omp/usage-reference.test.ts b/packages/server/src/server/agent/providers/omp/usage-reference.test.ts deleted file mode 100644 index 659c0afcea..0000000000 --- a/packages/server/src/server/agent/providers/omp/usage-reference.test.ts +++ /dev/null @@ -1,284 +0,0 @@ -import { existsSync, mkdtempSync, readFileSync, rmSync, mkdirSync, writeFileSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { createRequire } from "node:module"; -import { afterEach, expect, test } from "vitest"; -import { resolveOmpUsageReference } from "./usage-reference.js"; - -const { DatabaseSync } = createRequire(import.meta.url)("node:sqlite") as { - DatabaseSync: new (path: string) => { - exec(sql: string): void; - prepare(sql: string): { run(...args: unknown[]): void }; - close(): void; - }; -}; -// Upstream: packages/ai/src/auth/sqlite-credential-store.ts:576-581,711-724. -const OMP_AUTH_SCHEMA = ` - CREATE TABLE cache (key TEXT PRIMARY KEY, value TEXT NOT NULL, expires_at INTEGER NOT NULL); - CREATE INDEX idx_cache_expires ON cache(expires_at); - CREATE TABLE auth_credentials ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - provider TEXT NOT NULL, - credential_type TEXT NOT NULL, - data TEXT NOT NULL, - disabled_cause TEXT DEFAULT NULL, - identity_key TEXT DEFAULT NULL, - created_at INTEGER NOT NULL DEFAULT (CAST(strftime('%s','now') AS INTEGER)), - updated_at INTEGER NOT NULL DEFAULT (CAST(strftime('%s','now') AS INTEGER)) - ); -`; -const dirs: string[] = []; -const future = Date.now() + 3_600_000; -function fixture() { - const home = mkdtempSync(join(tmpdir(), "omp-usage-")); - dirs.push(home); - const agentDir = join(home, "agent"); - mkdirSync(agentDir); - const dbFile = join(agentDir, "agent.db"); - const db = new DatabaseSync(dbFile); - db.exec(OMP_AUTH_SCHEMA); - const env = { OMP_PROFILE: "", PI_CODING_AGENT_DIR: agentDir, XDG_DATA_HOME: "" }; - const row = ( - id: number, - provider = "openai-codex", - options: { disabled?: boolean; expires?: number; type?: string } = {}, - ) => - db - .prepare( - "INSERT INTO auth_credentials (id, provider, credential_type, data, disabled_cause) VALUES (?, ?, ?, ?, ?)", - ) - .run( - id, - provider, - options.type ?? "oauth", - JSON.stringify({ - access: `fixture-token-${id}`, - expires: options.expires ?? future, - accountId: `account-${id}`, - }), - options.disabled ? "disabled" : null, - ); - const sticky = (id: number, expires = Math.floor(Date.now() / 1000) + 100) => - db - .prepare("INSERT INTO cache (key, value, expires_at) VALUES (?, ?, ?)") - .run( - "session:sticky:openai-codex:session-1", - JSON.stringify({ type: "oauth", index: 0, credentialId: id }), - expires, - ); - const read = (override = {}) => - resolveOmpUsageReference("session-1", "openai-codex", { ...env, ...override }); - return { db, dbFile, agentDir, row, sticky, read }; -} -afterEach(() => { - for (const dir of dirs.splice(0)) rmSync(dir, { recursive: true, force: true }); -}); -test("sticky id selects the right OAuth row and leaves SQLite byte-identical", () => { - const f = fixture(); - f.row(1); - f.row(2); - f.sticky(2); - f.db.close(); - const before = readFileSync(f.dbFile); - expect(f.read()).toEqual({ - source: "codex", - input: { accessToken: "fixture-token-2", accountId: "account-2" }, - }); - expect(f.read()).toEqual({ - source: "codex", - input: { accessToken: "fixture-token-2", accountId: "account-2" }, - }); - expect(readFileSync(f.dbFile)).toEqual(before); -}); -test("single OAuth row resolves without sticky", () => { - const f = fixture(); - f.row(1); - f.db.close(); - expect(f.read()).toEqual({ - source: "codex", - input: { accessToken: "fixture-token-1", accountId: "account-1" }, - }); -}); -test("multiple rows without sticky are ambiguous", () => { - const f = fixture(); - f.row(1); - f.row(2); - f.db.close(); - expect(f.read()).toBeNull(); -}); -test("fallback counts only valid OAuth rows", () => { - const f = fixture(); - f.row(1); - f.row(2, "openai-codex", { expires: Date.now() - 1 }); - f.row(3, "openai-codex", { disabled: true }); - f.db - .prepare( - "INSERT INTO auth_credentials (id, provider, credential_type, data, disabled_cause) VALUES (?, ?, ?, ?, ?)", - ) - .run(4, "openai-codex", "oauth", "invalid-json", null); - f.db.close(); - expect(f.read()).toEqual({ - source: "codex", - input: { accessToken: "fixture-token-1", accountId: "account-1" }, - }); -}); -test("disabled and expired rows do not resolve", () => { - const a = fixture(); - a.row(1, "openai-codex", { disabled: true }); - a.sticky(1); - a.db.close(); - expect(a.read()).toBeNull(); - const b = fixture(); - b.row(1, "openai-codex", { expires: Date.now() - 1 }); - b.db.close(); - expect(b.read()).toBeNull(); -}); -test("expired sticky falls back to a single row", () => { - const f = fixture(); - f.row(1); - f.sticky(1, 1); - f.db.close(); - expect(f.read()?.source).toBe("codex"); -}); -test("env key does not displace OAuth with or without sticky", () => { - const f = fixture(); - f.row(1); - f.db.close(); - expect(f.read({ OPENAI_CODEX_OAUTH_TOKEN: "fixture-key" })?.source).toBe("codex"); - const g = fixture(); - g.row(1); - g.sticky(1); - g.db.close(); - expect(g.read({ OPENAI_CODEX_OAUTH_TOKEN: "fixture-key" })?.source).toBe("codex"); -}); -test("broker environment and config URLs block attribution", () => { - const f = fixture(); - f.row(1); - f.db.close(); - expect(f.read({ OMP_AUTH_BROKER_URL: "https://broker.invalid" })).toBeNull(); - writeFileSync( - join(f.agentDir, "config.yml"), - "auth:\n broker:\n url: https://broker.invalid\n", - ); - expect(f.read()).toBeNull(); -}); -test("broker config.yaml and flat keys block attribution with config.yml precedence", () => { - const f = fixture(); - f.row(1); - f.db.close(); - writeFileSync(join(f.agentDir, "config.yaml"), '"auth.broker.url": https://broker.invalid\n'); - expect(f.read()).toBeNull(); - writeFileSync(join(f.agentDir, "config.yml"), "auth:\n broker: {}\n"); - expect(f.read()).toEqual({ - source: "codex", - input: { accessToken: "fixture-token-1", accountId: "account-1" }, - }); - writeFileSync(join(f.agentDir, "config.yml"), '"auth.broker.url": https://broker.invalid\n'); - expect(f.read()).toBeNull(); -}); -test("models config API key overrides sticky OAuth", () => { - const f = fixture(); - f.row(1); - f.sticky(1); - f.db.close(); - writeFileSync( - join(f.agentDir, "models.yml"), - "providers:\n openai-codex:\n apiKey: fixture-key\n", - ); - expect(f.read()).toBeNull(); -}); -test("provider mismatch and malformed sticky fail closed", () => { - const f = fixture(); - f.row(1, "anthropic"); - f.sticky(1); - f.db.close(); - expect(f.read()).toBeNull(); -}); - -test("Anthropic OAuth maps to Claude and changes are read on each request", () => { - const f = fixture(); - f.row(1, "anthropic"); - f.db.close(); - expect( - resolveOmpUsageReference("session-1", "anthropic", { - OMP_PROFILE: "", - PI_CODING_AGENT_DIR: f.agentDir, - }), - ).toEqual({ - source: "claude", - input: { accessToken: "fixture-token-1" }, - }); - writeFileSync( - join(f.agentDir, "config.yml"), - "auth:\n broker:\n url: https://broker.invalid\n", - ); - expect( - resolveOmpUsageReference("session-1", "anthropic", { - OMP_PROFILE: "", - PI_CODING_AGENT_DIR: f.agentDir, - }), - ).toBeNull(); -}); - -test("named profile ignores agent override and uses existing XDG profile data", () => { - const f = fixture(); - f.db.close(); - const root = mkdtempSync(join(tmpdir(), "omp-profile-")); - dirs.push(root); - const data = join(root, "xdg", "omp", "profiles", "work"); - mkdirSync(data, { recursive: true }); - const profileDb = new DatabaseSync(join(data, "agent.db")); - profileDb.exec(OMP_AUTH_SCHEMA); - profileDb - .prepare( - "INSERT INTO auth_credentials (id, provider, credential_type, data, disabled_cause) VALUES (?, ?, ?, ?, ?)", - ) - .run( - 1, - "openai-codex", - "oauth", - JSON.stringify({ access: "profile-token", expires: future }), - null, - ); - profileDb.close(); - expect( - resolveOmpUsageReference( - "session-1", - "openai-codex", - { - OMP_PROFILE: "work", - PI_PROFILE: "other", - PI_CODING_AGENT_DIR: f.agentDir, - PI_CONFIG_DIR: join(root, "config"), - XDG_DATA_HOME: join(root, "xdg"), - }, - "linux", - ), - ).toEqual({ source: "codex", input: { accessToken: "profile-token" } }); -}); - -test("missing database returns null without creating a file", () => { - const home = mkdtempSync(join(tmpdir(), "omp-missing-db-")); - dirs.push(home); - const agentDir = join(home, "agent"); - mkdirSync(agentDir); - expect( - resolveOmpUsageReference("session-1", "openai-codex", { - OMP_PROFILE: "", - PI_CODING_AGENT_DIR: agentDir, - XDG_DATA_HOME: "", - }), - ).toBeNull(); - expect(existsSync(join(agentDir, "agent.db"))).toBe(false); -}); - -test("missing cache or credentials table returns null", () => { - const first = fixture(); - first.db.exec("DROP TABLE cache"); - first.db.close(); - expect(first.read()).toBeNull(); - const second = fixture(); - second.db.exec("DROP TABLE auth_credentials"); - second.db.close(); - expect(second.read()).toBeNull(); -}); diff --git a/packages/server/src/server/agent/providers/omp/usage-reference.ts b/packages/server/src/server/agent/providers/omp/usage-reference.ts deleted file mode 100644 index 384e59697e..0000000000 --- a/packages/server/src/server/agent/providers/omp/usage-reference.ts +++ /dev/null @@ -1,169 +0,0 @@ -import { existsSync, readFileSync } from "node:fs"; -import { createRequire } from "node:module"; -import { join } from "node:path"; - -import { z } from "zod"; -import { parse as parseYaml } from "yaml"; -import type { UsageReference } from "../../agent-sdk-types.js"; -import { resolveOmpDiagnosticPaths } from "./provider-config.js"; - -const stickySchema = z.object({ type: z.string(), credentialId: z.number().int().positive() }); -const rowSchema = z.object({ - id: z.number().int().positive(), - provider: z.string(), - credential_type: z.string(), - data: z.string(), - disabled_cause: z.string().nullable(), -}); -const oauthSchema = z.object({ - access: z.string().min(1), - expires: z.number(), - accountId: z.string().optional(), - orgId: z.string().optional(), -}); -const configSchema = z - .object({ - "auth.broker.url": z.string().optional(), - auth: z - .object({ broker: z.object({ url: z.string().optional() }).passthrough().optional() }) - .passthrough() - .optional(), - }) - .passthrough(); -const modelsSchema = z - .object({ - providers: z - .record(z.string(), z.object({ apiKey: z.unknown().optional() }).passthrough()) - .optional(), - }) - .passthrough(); - -interface UsageDb { - prepare(sql: string): { get(...args: unknown[]): unknown; all(...args: unknown[]): unknown[] }; - close(): void; -} -const localRequire = createRequire(import.meta.url); - -function readYaml(file: string): unknown { - try { - return parseYaml(readFileSync(file, "utf8")); - } catch { - return undefined; - } -} - -function hasKeyOverride(provider: string, env: NodeJS.ProcessEnv, agentDir: string): boolean { - // Upstream: packages/ai/src/auth-broker/discover.ts:87-110,197-225,321-329. - const configFile = ["config.yml", "config.yaml"] - .map((name) => join(agentDir, name)) - .find((file) => existsSync(file)); - const config = configSchema.safeParse(configFile ? readYaml(configFile) : undefined); - if ( - env.OMP_AUTH_BROKER_URL || - (config.success && (config.data.auth?.broker?.url ?? config.data["auth.broker.url"])) - ) - return true; - // Upstream: packages/ai/src/auth/cascade.ts:22-84. - const models = modelsSchema.safeParse(readYaml(join(agentDir, "models.yml"))); - return models.success && models.data.providers?.[provider]?.apiKey !== undefined; -} - -function readCredential( - db: UsageDb, - provider: string, - sessionId: string, -): z.infer | null { - // Upstream: packages/ai/src/auth/affinity.ts:83-88,115-132; sqlite-credential-store.ts:447-448,711-722. - const cache = db - .prepare( - "SELECT value FROM cache WHERE key = ? AND expires_at > CAST(strftime('%s','now') AS INTEGER)", - ) - .get(`session:sticky:${provider}:${sessionId}`) as { value?: unknown } | undefined; - if (cache) { - const sticky = stickySchema.safeParse(JSON.parse(String(cache.value))); - if (!sticky.success || sticky.data.type !== "oauth") return null; - return parseOAuthCredential( - db - .prepare( - "SELECT id, provider, credential_type, data, disabled_cause FROM auth_credentials WHERE id = ?", - ) - .get(sticky.data.credentialId), - provider, - ); - } - // Upstream: packages/ai/src/auth/cascade.ts:276-345 selects OAuth before env or stored API keys. - const rows = db - .prepare( - "SELECT id, provider, credential_type, data, disabled_cause FROM auth_credentials WHERE provider = ? AND credential_type = 'oauth' AND disabled_cause IS NULL", - ) - .all(provider); - const validRows = rows - .map((row) => parseOAuthCredential(row, provider)) - .filter((row): row is z.infer => row !== null); - return validRows.length === 1 ? validRows[0] : null; -} - -function parseOAuthCredential( - value: unknown, - provider: string, -): z.infer | null { - const row = rowSchema.safeParse(value); - if ( - !row.success || - row.data.provider !== provider || - row.data.credential_type !== "oauth" || - row.data.disabled_cause !== null - ) - return null; - try { - // Upstream: packages/ai/src/auth/sqlite-credential-store.ts:117-155 stores OAuth data as JSON without type. - const oauth = oauthSchema.safeParse(JSON.parse(row.data.data)); - return oauth.success && oauth.data.expires > Date.now() ? oauth.data : null; - } catch { - return null; - } -} - -/** Read OMP's current credential attribution without changing its store. */ -export function resolveOmpUsageReference( - sessionId: string, - provider: string, - env: NodeJS.ProcessEnv, - platform: NodeJS.Platform = process.platform, -): UsageReference | null { - let source: string; - if (provider === "openai-codex") source = "codex"; - else if (provider === "anthropic") source = "claude"; - else return null; - if (!sessionId) return null; - // Upstream: packages/utils/src/dirs.ts:82-94,330-343,583-585 (profile, override, agent dir). - const paths = resolveOmpDiagnosticPaths(env, undefined, platform); - if (hasKeyOverride(provider, env, paths.agentDir)) return null; - let db: UsageDb; - try { - const sqlite = localRequire("node:sqlite") as { - DatabaseSync: new (path: string, options: { readOnly: boolean }) => UsageDb; - }; - db = new sqlite.DatabaseSync(paths.agentDb, { readOnly: true }); - } catch { - return null; - } - try { - const oauth = readCredential(db, provider, sessionId); - if (!oauth) return null; - if (source === "claude") { - return { source, input: { accessToken: oauth.access } }; - } - return { - source, - input: { - accessToken: oauth.access, - ...(oauth.accountId ? { accountId: oauth.accountId } : {}), - }, - }; - } catch { - return null; - } finally { - db.close(); - } -} diff --git a/packages/server/src/server/agent/providers/opencode-agent.test.ts b/packages/server/src/server/agent/providers/opencode-agent.test.ts index 0634ec25cb..15bae2f56c 100644 --- a/packages/server/src/server/agent/providers/opencode-agent.test.ts +++ b/packages/server/src/server/agent/providers/opencode-agent.test.ts @@ -403,100 +403,6 @@ describe("OpenCodeAgentClient adapter smoke tests", () => { }, ); - test("usage reference follows the active OpenCode model and OAuth account", async () => { - const cwd = tmpCwd(); - const runtime = new TestOpenCodeHarness(); - const openCode = new TestOpenCodeClient(); - runtime.enqueueClient(openCode); - const client = new OpenCodeAgentClient( - logger, - { - env: { - OPENCODE_AUTH_CONTENT: JSON.stringify({ - openai: { type: "oauth", access: "oauth-token", accountId: "acct-1" }, - "opencode-go": { type: "api", key: "go-key" }, - }), - }, - }, - { serverManager: runtime, createClient: runtime.createClient }, - ); - const session = await client.createSession(buildConfig(cwd)); - await session.setModel?.("openai/gpt-5"); - expect(await session.getUsageReference?.()).toEqual({ - source: "codex", - input: { accessToken: "oauth-token", accountId: "acct-1" }, - }); - await session.setModel?.("opencode-go/qwen"); - expect(await session.getUsageReference?.()).toEqual({ - source: "opencode-go", - input: { apiKey: "go-key" }, - }); - await session.setModel?.("other/model"); - expect(await session.getUsageReference?.()).toBeNull(); - const events: AgentStreamEvent[] = []; - session.subscribe((event) => events.push(event)); - const sessionId = (await session.getRuntimeInfo()).sessionId; - openCode.emitEvent({ - type: "message.updated", - properties: { - info: { - id: "msg_changed_model", - sessionID: sessionId, - role: "user", - model: { providerID: "openai", modelID: "gpt-5" }, - }, - }, - } as OpenCodeEvent); - await vi.waitFor(async () => - expect(await session.getUsageReference?.()).toEqual({ - source: "codex", - input: { accessToken: "oauth-token", accountId: "acct-1" }, - }), - ); - expect(events).toContainEqual({ - type: "model_changed", - provider: "opencode", - runtimeInfo: { - provider: "opencode", - sessionId, - model: "openai/gpt-5", - modeId: null, - thinkingOptionId: null, - }, - }); - await session.close(); - rmSync(cwd, { recursive: true, force: true }); - }); - - test("usage reference ignores another auth entry with a different type", async () => { - const cwd = tmpCwd(); - const runtime = new TestOpenCodeHarness(); - runtime.enqueueClient(new TestOpenCodeClient()); - const client = new OpenCodeAgentClient( - logger, - { - env: { - OPENCODE_AUTH_CONTENT: JSON.stringify({ - openai: { type: "api", key: "other-key" }, - "opencode-go": { type: "api", key: "go-key" }, - }), - }, - }, - { serverManager: runtime, createClient: runtime.createClient }, - ); - const session = await client.createSession(buildConfig(cwd)); - try { - await session.setModel?.("opencode-go/qwen"); - expect(await session.getUsageReference?.()).toEqual({ - source: "opencode-go", - input: { apiKey: "go-key" }, - }); - } finally { - await session.close(); - rmSync(cwd, { recursive: true, force: true }); - } - }); - test("creates a session with valid id and provider", async () => { const cwd = tmpCwd(); const runtime = new TestOpenCodeHarness(); diff --git a/packages/server/src/server/agent/providers/opencode-agent.ts b/packages/server/src/server/agent/providers/opencode-agent.ts index b2cab99c6e..70f7f367e1 100644 --- a/packages/server/src/server/agent/providers/opencode-agent.ts +++ b/packages/server/src/server/agent/providers/opencode-agent.ts @@ -12,8 +12,6 @@ import { type TextPartInput as OpenCodeTextPartInput, } from "@opencode-ai/sdk/v2/client"; import fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; import { randomBytes } from "node:crypto"; import { isDeepStrictEqual } from "node:util"; import { createPathEquivalenceMatcher } from "../../../utils/path.js"; @@ -58,7 +56,6 @@ import { type SteerResult, type ToolCallDetail, type ToolCallTimelineItem, - type UsageReference, } from "../agent-sdk-types.js"; import { importSessionFromPersistence } from "../provider-session-import.js"; import { @@ -1503,7 +1500,6 @@ export class OpenCodeAgentClient implements AgentClient { false, unbindBridge, connectServer, - { ...process.env, ...this.runtimeSettings?.env, ...launchContext?.env }, ); } catch (error) { await connection.release(); @@ -1558,7 +1554,6 @@ export class OpenCodeAgentClient implements AgentClient { registeredAcquisition !== null, unbindBridge, connectServer, - { ...process.env, ...this.runtimeSettings?.env, ...launchContext?.env }, ); } catch (error) { await connection.release(); @@ -3365,67 +3360,6 @@ interface OpenCodeServerConnection { release: () => Promise; } -const openCodeOAuthAuthSchema = z - .object({ - openai: z - .object({ - type: z.literal("oauth"), - access: z.string().min(1), - accountId: z.string().optional(), - }) - .passthrough() - .optional(), - }) - .passthrough(); -const openCodeGoAuthSchema = z - .object({ - "opencode-go": z - .object({ type: z.literal("api"), key: z.string().min(1) }) - .passthrough() - .optional(), - }) - .passthrough(); - -async function readOpenCodeUsageAuth(env: NodeJS.ProcessEnv): Promise { - try { - const content = - env.OPENCODE_AUTH_CONTENT ?? - (await fs.readFile( - path.join( - env.XDG_DATA_HOME || path.join(env.HOME || os.homedir(), ".local", "share"), - "opencode", - "auth.json", - ), - "utf8", - )); - return JSON.parse(content); - } catch { - return null; - } -} - -function resolveOpenCodeUsageReference(model: string, auth: unknown): UsageReference | null { - if (!auth) return null; - if (model.startsWith("openai/")) { - const parsed = openCodeOAuthAuthSchema.safeParse(auth); - if (!parsed.success) return null; - const credential = parsed.data.openai; - if (!credential) return null; - return { - source: "codex", - input: { - accessToken: credential.access, - ...(credential.accountId ? { accountId: credential.accountId } : {}), - }, - }; - } - const parsed = openCodeGoAuthSchema.safeParse(auth); - if (!parsed.success) return null; - const credential = parsed.data["opencode-go"]; - if (!credential) return null; - return { source: "opencode-go", input: { apiKey: credential.key } }; -} - class OpenCodeAgentSession implements AgentSession { readonly provider = "opencode" as const; readonly capabilities = OPENCODE_CAPABILITIES; @@ -3514,7 +3448,6 @@ class OpenCodeAgentSession implements AgentSession { private readonly externallyDriven = false, releaseBridge?: () => void, connectServer?: () => Promise, - private readonly usageEnv: NodeJS.ProcessEnv = process.env, ) { this.config = config; this.server = { client, events, url: serverUrl ?? "", release: releaseServer }; @@ -3620,13 +3553,6 @@ class OpenCodeAgentSession implements AgentSession { }; } - async getUsageReference(): Promise { - const model = this.config.model; - if (!model?.startsWith("openai/") && !model?.startsWith("opencode-go/")) return null; - const auth = await readOpenCodeUsageAuth(this.usageEnv); - return resolveOpenCodeUsageReference(model, auth); - } - async setModel(modelId: string | null): Promise { await this.reconnectIfServerExited(); const normalizedModelId = diff --git a/packages/server/src/server/agent/providers/opencode-bridge.local.e2e.test.ts b/packages/server/src/server/agent/providers/opencode-bridge.local.e2e.test.ts index c8146bf2d1..a441cebcb5 100644 --- a/packages/server/src/server/agent/providers/opencode-bridge.local.e2e.test.ts +++ b/packages/server/src/server/agent/providers/opencode-bridge.local.e2e.test.ts @@ -355,7 +355,8 @@ test.each([ await resumed?.close(); await original?.close(); await client.shutdown(); - await rm(root, { recursive: true, force: true }); + // Windows can retain a transient executable lock after provider shutdown. + await rm(root, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); } }, 240_000, diff --git a/packages/server/src/server/agent/providers/pi/agent.test.ts b/packages/server/src/server/agent/providers/pi/agent.test.ts index 7308185db0..969b221f02 100644 --- a/packages/server/src/server/agent/providers/pi/agent.test.ts +++ b/packages/server/src/server/agent/providers/pi/agent.test.ts @@ -23,7 +23,7 @@ import { PiRpcAgentSession, transformPiModels, } from "./agent.js"; -import { FakePi, type FakePiSession } from "./test-utils/fake-pi.js"; +import { FakePi } from "./test-utils/fake-pi.js"; import { createPiExtensionHost } from "./extensions/index.js"; import { PiExtensionHost } from "./extensions/host.js"; import type { PiModel, PiThinkingLevel } from "./rpc-types.js"; @@ -202,71 +202,6 @@ async function createSession( return { pi, session, events }; } -test("Pi usage reference follows the current OAuth model and agent directory", async () => { - const agentDir = mkdtempSync(path.join(tmpdir(), "paseo-pi-usage-")); - try { - writeFileSync( - path.join(agentDir, "auth.json"), - JSON.stringify({ - "openai-codex": { - type: "oauth", - access: "codex-token", - accountId: "account-1", - refresh: "refresh", - expires: Date.now() + 60_000, - }, - anthropic: { - type: "oauth", - access: "claude-token", - refresh: "refresh", - expires: Date.now() + 60_000, - }, - }), - ); - const pi = new FakePi(); - let runtime!: FakePiSession; - pi.queueSessionSetup((sessionRuntime) => { - runtime = sessionRuntime; - runtime.state.model = { provider: "openai-codex", id: "gpt", name: "GPT" }; - }); - const session = await createClient(pi).createSession(createConfig(), { - env: { PI_CODING_AGENT_DIR: agentDir }, - }); - expect(await session.getUsageReference?.()).toEqual({ - source: "codex", - input: { accessToken: "codex-token", accountId: "account-1" }, - }); - runtime.state.model = { provider: "anthropic", id: "claude", name: "Claude" }; - expect(await session.getUsageReference?.()).toEqual({ - source: "claude", - input: { accessToken: "claude-token" }, - }); - writeFileSync( - path.join(agentDir, "auth.json"), - JSON.stringify({ - "openai-codex": { type: "oauth", access: "codex-token" }, - anthropic: { type: "api_key", key: "api-key" }, - }), - ); - runtime.state.model = { provider: "openai-codex", id: "gpt", name: "GPT" }; - expect(await session.getUsageReference?.()).toEqual({ - source: "codex", - input: { accessToken: "codex-token" }, - }); - runtime.state.model = { provider: "anthropic", id: "claude", name: "Claude" }; - writeFileSync( - path.join(agentDir, "auth.json"), - JSON.stringify({ anthropic: { type: "api_key", key: "api-key" } }), - ); - expect(await session.getUsageReference?.()).toBeNull(); - runtime.state.model = { provider: "other", id: "other", name: "Other" }; - expect(await session.getUsageReference?.()).toBeNull(); - await session.close(); - } finally { - rmSync(agentDir, { recursive: true, force: true }); - } -}); - test("forwards launch-context env to the Pi process launch", async () => { const pi = new FakePi(); const client = createClient(pi); diff --git a/packages/server/src/server/agent/providers/pi/agent.ts b/packages/server/src/server/agent/providers/pi/agent.ts index 5d7966367e..5fd7b051ef 100644 --- a/packages/server/src/server/agent/providers/pi/agent.ts +++ b/packages/server/src/server/agent/providers/pi/agent.ts @@ -1,6 +1,5 @@ import { randomUUID } from "node:crypto"; import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; -import { readFile } from "node:fs/promises"; import { homedir, tmpdir } from "node:os"; import { join, resolve as resolvePath } from "node:path"; import type { Logger } from "pino"; @@ -243,7 +242,6 @@ interface PiRpcAgentSessionOptions { extensionTimeoutMs?: number; logger: Logger; usagePollScheduler?: PiUsagePollScheduler; - usageEnv?: Record; } interface PiResumeConfig { @@ -530,27 +528,6 @@ function resolvePiAgentDir(env: Record | undefined): string { return resolvePath(configured); } -const piCodexUsageAuthSchema = z - .object({ - "openai-codex": z - .object({ - type: z.literal("oauth"), - access: z.string().min(1), - accountId: z.string().optional(), - }) - .passthrough() - .optional(), - }) - .passthrough(); -const piClaudeUsageAuthSchema = z - .object({ - anthropic: z - .object({ type: z.literal("oauth"), access: z.string().min(1) }) - .passthrough() - .optional(), - }) - .passthrough(); - function readPiGlobalMcpConfig(env: Record | undefined): Record { const globalConfigPath = join(resolvePiAgentDir(env), "mcp.json"); if (!existsSync(globalConfigPath)) { @@ -1145,35 +1122,6 @@ export class PiRpcAgentSession implements AgentSession { readonly provider: AgentProvider; readonly capabilities: AgentCapabilityFlags; - async getUsageReference() { - await this.refreshState(); - const provider = this.state.model?.provider; - let source: string | null = null; - if (provider === "openai-codex") source = "codex"; - if (provider === "anthropic") source = "claude"; - if (!source || !provider) return null; - try { - const auth: unknown = JSON.parse( - await readFile(join(resolvePiAgentDir(this.usageEnv), "auth.json"), "utf8"), - ); - if (provider === "openai-codex") { - const credential = piCodexUsageAuthSchema.parse(auth)["openai-codex"]; - if (!credential) return null; - return { - source, - input: { - accessToken: credential.access, - ...(credential.accountId ? { accountId: credential.accountId } : {}), - }, - }; - } - const credential = piClaudeUsageAuthSchema.parse(auth).anthropic; - return credential ? { source, input: { accessToken: credential.access } } : null; - } catch { - return null; - } - } - private readonly subscribers = new Set<(event: AgentStreamEvent) => void>(); private readonly activeToolCalls = new Map(); private readonly pendingExtensionUiRequests = new Map(); @@ -1201,7 +1149,6 @@ export class PiRpcAgentSession implements AgentSession { private readonly currentModeId: string | null; private readonly logger: Logger; private readonly usagePoller: PiUsagePoller; - private readonly usageEnv?: Record; private closed = false; private readonly closeController = new AbortController(); private readonly pendingExtensionHydrations = new Set>(); @@ -1221,7 +1168,6 @@ export class PiRpcAgentSession implements AgentSession { this.extensionTimeoutMs = options.extensionTimeoutMs ?? DEFAULT_PI_EXTENSION_RESULT_TIMEOUT_MS; this.logger = options.logger; this.extensionHost = createPiExtensionHost(this.logger); - this.usageEnv = options.usageEnv; this.usagePoller = new PiUsagePoller({ scheduler: options.usagePollScheduler, readStats: () => this.runtimeSession.getSessionStats(), @@ -2515,7 +2461,6 @@ export class PiRpcAgentClient implements AgentClient { extensionTimeoutMs: this.providerParams.extensionTimeoutMs, logger: this.logger, usagePollScheduler: this.usagePollScheduler, - usageEnv: mcpEnv, }); } catch (error) { await runtimeSession.close().catch(() => undefined); @@ -2579,7 +2524,6 @@ export class PiRpcAgentClient implements AgentClient { extensionTimeoutMs: this.providerParams.extensionTimeoutMs, logger: this.logger, usagePollScheduler: this.usagePollScheduler, - usageEnv: mcpEnv, }); } catch (error) { await runtimeSession.close().catch(() => undefined); diff --git a/packages/server/src/server/authorization/operation-permissions.ts b/packages/server/src/server/authorization/operation-permissions.ts index fa260011c3..214544c39c 100644 --- a/packages/server/src/server/authorization/operation-permissions.ts +++ b/packages/server/src/server/authorization/operation-permissions.ts @@ -150,7 +150,6 @@ const INBOUND_PERMISSION = { project_icon_request: "workspace.read", "provider.usage.list.request": "daemon.read", "usage.list_reports.request": "daemon.read", - "agent.resolve_usage_report.request": "daemon.read", provider_diagnostic_request: "daemon.read", pull_request_timeline_request: "workspace.read", "push.unregister.request": "workspace.read", @@ -376,7 +375,6 @@ const OUTBOUND_PERMISSION = { project_icon_response: "workspace.read", "provider.usage.list.response": "daemon.read", "usage.list_reports.response": "daemon.read", - "agent.resolve_usage_report.response": "daemon.read", provider_diagnostic_response: "daemon.read", providers_snapshot_update: ["daemon.read", "hub.execute"], pull_request_timeline_response: "workspace.read", diff --git a/packages/server/src/server/plugins/index.ts b/packages/server/src/server/plugins/index.ts index f11b7c50a4..830d12251a 100644 --- a/packages/server/src/server/plugins/index.ts +++ b/packages/server/src/server/plugins/index.ts @@ -28,7 +28,6 @@ import type { PluginProviderMetadata } from "./plugin-process-protocol.js"; import { readPluginProviderIcon } from "./provider-icon.js"; import { UsageSourceRegistry } from "./usage-sources/index.js"; import type { PluginUsageSourceMetadata } from "./plugin-process-protocol.js"; -import type { UsageReference } from "../agent/agent-sdk-types.js"; const BUILTIN_PROVIDER_ID_SET: ReadonlySet = new Set(BUILTIN_PROVIDER_IDS); @@ -144,18 +143,10 @@ export class PluginService { return [...this.providers.values()].sort((left, right) => left.id.localeCompare(right.id)); } - listUsageReports(options?: { - forceRefresh?: boolean; - reportIds?: string[]; - references?: UsageReference[]; - }) { + listUsageReports(options?: { forceRefresh?: boolean; reportIds?: string[] }) { return this.usageSources.listReports(options); } - resolveUsageReference(reference: UsageReference) { - return this.usageSources.resolveReference(reference); - } - listLegacyUsage() { return this.usageSources.listLegacyUsage(); } @@ -577,7 +568,6 @@ export class PluginService { label: source.label, icon: source.icon, discover: async () => { - if (!source.discover) return []; const result = await this.runtime.discoverUsage(pluginId, source.id); if (!Array.isArray(result)) throw new Error(`Invalid usage discovery from ${source.id}`); diff --git a/packages/server/src/server/plugins/plugin-process-protocol.ts b/packages/server/src/server/plugins/plugin-process-protocol.ts index 16ee512c37..cf1cd2c32a 100644 --- a/packages/server/src/server/plugins/plugin-process-protocol.ts +++ b/packages/server/src/server/plugins/plugin-process-protocol.ts @@ -19,7 +19,6 @@ export interface PluginUsageSourceMetadata { id: string; label: string; icon?: string; - discover: boolean; } export type PluginProcessRequest = @@ -108,7 +107,6 @@ const usageSourceMetadataSchema = z id: z.string().min(1), label: z.string().min(1), icon: z.string().optional(), - discover: z.boolean(), }) .strict(); const providerConnectRequestSchema = z diff --git a/packages/server/src/server/plugins/plugin-process.ts b/packages/server/src/server/plugins/plugin-process.ts index 1ed1bd6fc3..2c371d6ba1 100644 --- a/packages/server/src/server/plugins/plugin-process.ts +++ b/packages/server/src/server/plugins/plugin-process.ts @@ -133,6 +133,7 @@ export function createPluginWorker(options: { !/^[a-z][a-z0-9._-]*$/.test(id) || !source.label.trim() || typeof source.fetch !== "function" || + typeof source.discover !== "function" || !source.input || typeof source.input.parseAsync !== "function" ) { @@ -289,7 +290,6 @@ export function createPluginWorker(options: { icon: source.icon ? await readPluginProviderIcon(message.pluginDirectory, source.icon) : undefined, - discover: !!source.discover, })), ); send({ @@ -336,8 +336,7 @@ export function createPluginWorker(options: { void (async () => { const source = usageSources.get(message.sourceId); if (!source) throw new Error(`Unknown usage source: ${message.sourceId}`); - if (message.type === "usage.discover") - return jsonTransportValue(source.discover ? await source.discover() : []); + if (message.type === "usage.discover") return jsonTransportValue(await source.discover()); const input = await source.input.parseAsync(message.input); return jsonTransportValue( message.type === "usage.identify" diff --git a/packages/server/src/server/plugins/runtime.posix.test.ts b/packages/server/src/server/plugins/runtime.posix.test.ts index c09ef9ebe2..5615bef6ac 100644 --- a/packages/server/src/server/plugins/runtime.posix.test.ts +++ b/packages/server/src/server/plugins/runtime.posix.test.ts @@ -2030,3 +2030,29 @@ export default function contribute(plugin: any) { } }); }); + +it("rejects a usage source without discovery at registration", async () => { + const directory = await createPlugin( + "missing-discovery", + ` +export default function contribute(server) { + server.registerUsageSource({ + id: "missing-discovery", + label: "Missing discovery", + input: { parseAsync: async (value) => value }, + identify: async () => ({ key: "account" }), + fetch: async () => ({ status: "available", windows: [] }), + }); + return () => {}; +} +`, + ); + const runtime = createTestRuntime(); + try { + await expect(runtime.startPlugin("missing-discovery", directory)).rejects.toThrow( + "Invalid usage source: missing-discovery", + ); + } finally { + await runtime.stopAll(); + } +}); diff --git a/packages/server/src/server/plugins/session-usage-reference.e2e.test.ts b/packages/server/src/server/plugins/session-usage-reference.e2e.test.ts deleted file mode 100644 index c4f2287bbf..0000000000 --- a/packages/server/src/server/plugins/session-usage-reference.e2e.test.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { fileURLToPath } from "node:url"; -import { expect, test } from "vitest"; -import { ACPAgentSession } from "../agent/providers/acp-agent.js"; -import type { UsageReference } from "../agent/agent-sdk-types.js"; -import { ClaudeAgentClient } from "../agent/providers/claude/agent.js"; -import { CodexAppServerAgentSession } from "../agent/providers/codex-app-server-agent.js"; -import { createTestLogger } from "../../test-utils/test-logger.js"; -import { DaemonClient } from "../test-utils/daemon-client.js"; -import { createTestAgentClient } from "../test-utils/fake-agent-client.js"; -import { createTestPaseoDaemon } from "../test-utils/paseo-daemon.js"; -import { BuiltinPluginLoader } from "./builtin/index.js"; - -const fixtureRoot = fileURLToPath(new URL("./test-fixtures/", import.meta.url)); -const fixtureBuiltins = () => new BuiltinPluginLoader(fixtureRoot, ["session-usage-reference"]); - -test("resolves a provider plugin session reference through its usage source", async () => { - const directory = fileURLToPath( - new URL("./test-fixtures/session-usage-reference/", import.meta.url), - ); - const daemon = await createTestPaseoDaemon({ - pluginsEnabled: false, - builtinPlugins: fixtureBuiltins(), - }); - const client = new DaemonClient({ url: `ws://127.0.0.1:${daemon.port}/ws` }); - try { - await client.connect(); - const agent = await client.createAgent({ - provider: "fixture-session-provider", - cwd: directory, - }); - const result = await client.resolveAgentUsageReport({ agentId: agent.id }); - expect(result.reportId).toBe("fixture-session-usage:from-session"); - expect( - (await client.listUsageReports({ reportIds: [result.reportId!] })).reports[0], - ).toMatchObject({ - id: result.reportId, - sourceId: "fixture-session-usage", - report: { windows: [{ usedPct: 31 }] }, - }); - const reports = await client.listUsageReports(); - expect(reports.reports.map((entry) => entry.id)).toContain( - "fixture-session-usage:from-session", - ); - const missing = await client.createAgent({ - provider: "fixture-session-provider", - cwd: directory, - model: "missing", - }); - expect((await client.resolveAgentUsageReport({ agentId: missing.id })).reportId).toBeNull(); - } finally { - await client.close(); - await daemon.close(); - } -}, 60_000); - -test("agent.resolve_usage_report resolves source IDs from default built-in and ACP sessions", async () => { - const directory = fileURLToPath( - new URL("./test-fixtures/session-usage-reference/", import.meta.url), - ); - const providers = ["claude", "codex", "copilot", "cursor", "kimi", "generic-acp"] as const; - const logger = createTestLogger(); - const references = new Map(); - const claude = await new ClaudeAgentClient({ - logger, - resolveBinary: async () => "/test/claude/bin", - }).createSession( - { provider: "claude", cwd: directory }, - { - env: { - HOME: directory, - CLAUDE_CONFIG_DIR: "", - ANTHROPIC_BASE_URL: "", - ANTHROPIC_API_KEY: "", - ANTHROPIC_AUTH_TOKEN: "", - }, - }, - ); - references.set("claude", (await claude.getUsageReference?.()) ?? null); - await claude.close(); - const codex = new CodexAppServerAgentSession( - { provider: "codex", cwd: directory }, - null, - logger, - () => { - throw new Error("Codex runtime should not start"); - }, - {}, - false, - false, - false, - undefined, - "interactive", - { HOME: directory, OPENAI_BASE_URL: "" }, - ); - references.set("codex", await codex.getUsageReference()); - for (const provider of providers.slice(2)) { - const session = new ACPAgentSession( - { provider, cwd: directory }, - { - provider, - logger, - defaultCommand: ["unused"], - defaultModes: [], - capabilities: { - supportsStreaming: true, - supportsSessionPersistence: true, - supportsDynamicModes: true, - supportsMcpServers: true, - supportsReasoningStream: true, - supportsToolInvocations: true, - }, - }, - ); - references.set(provider, await session.getUsageReference()); - } - const agentClients = Object.fromEntries( - providers.map((provider) => { - const client = createTestAgentClient(provider); - const createSession = client.createSession.bind(client); - client.createSession = async (...args) => { - const session = await createSession(...args); - session.getUsageReference = async () => references.get(provider) ?? null; - return session; - }; - return [provider, client]; - }), - ); - const daemon = await createTestPaseoDaemon({ - pluginsEnabled: false, - builtinPlugins: fixtureBuiltins(), - agentClients, - providerOverrides: { - cursor: { extends: "acp", label: "Cursor", command: ["cursor-agent", "acp"] }, - kimi: { extends: "acp", label: "Kimi", command: ["kimi", "acp"] }, - "generic-acp": { extends: "acp", label: "Generic ACP", command: ["generic-acp", "acp"] }, - }, - }); - const client = new DaemonClient({ url: `ws://127.0.0.1:${daemon.port}/ws` }); - try { - await client.connect(); - for (const provider of providers) { - const agent = await client.createAgent({ provider, cwd: directory }); - const result = await client.resolveAgentUsageReport({ agentId: agent.id }); - expect(result.reportId, provider).toBe(`${provider}:default`); - } - } finally { - await client.close(); - await daemon.close(); - } -}, 60_000); diff --git a/packages/server/src/server/plugins/test-fixtures/session-usage-reference/index.server.ts b/packages/server/src/server/plugins/test-fixtures/session-usage-reference/index.server.ts deleted file mode 100644 index c5aeaede16..0000000000 --- a/packages/server/src/server/plugins/test-fixtures/session-usage-reference/index.server.ts +++ /dev/null @@ -1,101 +0,0 @@ -import type { PluginServerContext } from "@getpaseo/plugin/server"; -import { - negotiateProviderCapabilities, - type ProviderEvent, -} from "@getpaseo/plugin/server/provider"; -import { z } from "zod"; - -export default function contribute(server: PluginServerContext) { - for (const id of ["claude", "codex", "copilot", "cursor", "kimi", "generic-acp"]) { - let input = z.object({}).strict(); - if (id === "claude") input = z.object({ configDir: z.string() }).strict(); - if (id === "codex") input = z.object({ codexHome: z.string() }).strict(); - server.registerUsageSource({ - id, - label: id, - input, - identify: async () => ({ key: "default" }), - fetch: async () => ({ - status: "available", - windows: [{ id: "hour", label: "Hour", usedPct: 31, headline: true }], - }), - }); - } - server.registerUsageSource({ - id: "fixture-session-usage", - label: "Fixture session usage", - input: z.object({ account: z.string() }).strict(), - identify: async (input) => ({ key: (input as { account: string }).account }), - fetch: async () => ({ - status: "available", - windows: [{ id: "hour", label: "Hour", usedPct: 31, headline: true }], - }), - }); - server.registerProvider({ - id: "fixture-session-provider", - label: "Fixture session provider", - async connect(request) { - const capabilities = negotiateProviderCapabilities(request.capabilities, [ - "prompt.message", - "session.usage_reference", - ]); - const listeners = new Set<(event: ProviderEvent) => void>(); - const models = new Map(); - const emit = (event: ProviderEvent) => { - for (const listener of listeners) listener(event); - }; - return { - version: 1, - capabilities, - async send(input) { - if (input.type === "catalog") - emit({ - type: "catalog", - requestId: input.requestId, - catalog: { - models: [ - { id: "fixture", label: "Fixture" }, - { id: "missing", label: "Missing" }, - ], - modes: [], - }, - }); - if (input.type === "session.open") { - models.set(input.sessionId, input.config.model); - emit({ - type: "session.opened", - requestId: input.requestId, - sessionId: input.sessionId, - capabilities, - restoration: "core", - cwd: input.config.cwd, - }); - emit({ type: "session.ready", requestId: input.requestId, sessionId: input.sessionId }); - } - if (input.type === "session.usage_reference") - emit({ - type: "usage_reference", - requestId: input.requestId, - reference: { - source: - models.get(input.sessionId) === "missing" - ? "missing-source" - : "fixture-session-usage", - input: { account: "from-session" }, - }, - }); - if (input.type === "session.close") - emit({ type: "request.completed", requestId: input.requestId }); - }, - onEvent(listener) { - listeners.add(listener); - return () => listeners.delete(listener); - }, - async close() { - listeners.clear(); - }, - }; - }, - }); - return () => {}; -} diff --git a/packages/server/src/server/plugins/test-fixtures/session-usage-reference/paseo-plugin.json b/packages/server/src/server/plugins/test-fixtures/session-usage-reference/paseo-plugin.json deleted file mode 100644 index f7b37baee9..0000000000 --- a/packages/server/src/server/plugins/test-fixtures/session-usage-reference/paseo-plugin.json +++ /dev/null @@ -1 +0,0 @@ -{ "id": "session-usage-reference", "requirements": { "paseo": ">=0.9.2" } } diff --git a/packages/server/src/server/plugins/test-fixtures/usage-source-directory/index.server.ts b/packages/server/src/server/plugins/test-fixtures/usage-source-directory/index.server.ts index 48a4ee5eec..5845073a7c 100644 --- a/packages/server/src/server/plugins/test-fixtures/usage-source-directory/index.server.ts +++ b/packages/server/src/server/plugins/test-fixtures/usage-source-directory/index.server.ts @@ -23,7 +23,7 @@ export default function contribute(server: PluginServerContext) { fetches++; return { status: "available", - windows: [{ id: "count", label: "Count", usedPct: fetches, headline: true }], + windows: [{ id: "count", label: "Count", usedPct: fetches }], }; }, }); diff --git a/packages/server/src/server/plugins/test-fixtures/usage-source/index.server.ts b/packages/server/src/server/plugins/test-fixtures/usage-source/index.server.ts index a8f1f28211..347e8050d0 100644 --- a/packages/server/src/server/plugins/test-fixtures/usage-source/index.server.ts +++ b/packages/server/src/server/plugins/test-fixtures/usage-source/index.server.ts @@ -23,7 +23,7 @@ export default function contribute(server: PluginServerContext) { fetches++; return { status: "available", - windows: [{ id: "count", label: "Count", usedPct: fetches, headline: true }], + windows: [{ id: "count", label: "Count", usedPct: fetches }], }; }, }); diff --git a/packages/server/src/server/plugins/usage-sources/index.test.ts b/packages/server/src/server/plugins/usage-sources/index.test.ts index 2b8d0b672b..41fa0ef2cb 100644 --- a/packages/server/src/server/plugins/usage-sources/index.test.ts +++ b/packages/server/src/server/plugins/usage-sources/index.test.ts @@ -4,7 +4,7 @@ import { UsageSourceRegistry } from "./index.js"; function source(input: { id: string; discover?: () => Promise; - identify?: (value: unknown) => Promise<{ key: string } | null>; + identify?: (value: unknown) => Promise<{ key: string; label?: string } | null>; fetch?: (value: unknown) => Promise; }) { return { @@ -18,35 +18,26 @@ function source(input: { }; } -test("identifies accounts without fetching and preserves IDs across token rotation", async () => { +test("discovery preserves account IDs and updates input after token rotation", async () => { const registry = new UsageSourceRegistry(); - let fetches = 0; let token = "old"; registry.register( source({ id: "codex", discover: async () => [{ account: "work", token }], - fetch: async (input) => { - fetches++; - return { - status: "available", - windows: [{ id: "token", label: (input as { token: string }).token }], - }; - }, + fetch: async (input) => ({ + status: "available", + windows: [{ id: "token", label: (input as { token: string }).token }], + }), }), ); - expect( - await registry.resolveReference({ source: "codex", input: { account: "work", token } }), - ).toBe("codex:work"); - expect(fetches).toBe(0); + const first = await registry.listReports(); + expect(first.map((entry) => entry.id)).toEqual(["codex:work"]); + expect(first[0]?.report.windows[0]?.label).toBe("old"); token = "new"; - expect( - await registry.resolveReference({ source: "codex", input: { account: "work", token } }), - ).toBe("codex:work"); - expect( - (await registry.listReports({ reportIds: ["codex:work"] }))[0]?.report.windows[0]?.label, - ).toBe("new"); - expect(fetches).toBe(1); + const refreshed = await registry.listReports({ forceRefresh: true }); + expect(refreshed.map((entry) => entry.id)).toEqual(["codex:work"]); + expect(refreshed[0]?.report.windows[0]?.label).toBe("new"); }); test("coalesces per account, caches errors, and refreshes only requested IDs", async () => { @@ -100,9 +91,6 @@ test("invalid keys become source errors and missing identities produce no report const reports = await registry.listReports(); expect(reports.map((entry) => entry.id)).toEqual(["source:!error"]); expect(reports[0]?.report.status).toBe("error"); - expect( - await registry.resolveReference({ source: "source", input: { account: "none" } }), - ).toBeNull(); }); test("legacy listing uses oldest fetchedAt", async () => { @@ -111,62 +99,12 @@ test("legacy listing uses oldest fetchedAt", async () => { registry.register( source({ id: "source", discover: async () => [{ account: "a" }, { account: "b" }] }), ); - await registry.listReports({ references: [{ source: "source", input: { account: "a" } }] }); + await registry.listReports(); now = 2000; await registry.listReports({ reportIds: ["source:b"], forceRefresh: true }); expect((await registry.listLegacyUsage()).fetchedAt).toBe(new Date(1000).toISOString()); }); -test("two Codex homes and a token route resolve to their vendor account IDs", async () => { - const { mkdtemp, writeFile, rm } = await import("node:fs/promises"); - const { tmpdir } = await import("node:os"); - const { join } = await import("node:path"); - const { identify } = await import("../../../../../../plugins/codex-usage-source/server/usage.js"); - const personal = await mkdtemp(join(tmpdir(), "usage-personal-")); - const work = await mkdtemp(join(tmpdir(), "usage-work-")); - try { - const token = (accountId: string, email: string) => - `header.${Buffer.from(JSON.stringify({ "https://api.openai.com/auth": { chatgpt_account_id: accountId }, email })).toString("base64url")}.signature`; - await writeFile( - join(personal, "auth.json"), - JSON.stringify({ - tokens: { - account_id: "personal-id", - access_token: token("personal-id", "personal@example.test"), - }, - }), - ); - await writeFile( - join(work, "auth.json"), - JSON.stringify({ - tokens: { account_id: "work-id", access_token: token("work-id", "work@example.test") }, - }), - ); - const registry = new UsageSourceRegistry(); - registry.register( - source({ - id: "codex", - discover: async () => [{ codexHome: personal }, { codexHome: work }], - identify: async (input) => identify(input as Parameters[0]), - }), - ); - - expect( - await registry.resolveReference({ - source: "codex", - input: { accessToken: token("work-id", "work@example.test") }, - }), - ).toBe("codex:work-id"); - expect((await registry.listReports()).map((entry) => [entry.id, entry.account.label])).toEqual([ - ["codex:personal-id", "personal@example.test"], - ["codex:work-id", "work@example.test"], - ]); - } finally { - await rm(personal, { recursive: true, force: true }); - await rm(work, { recursive: true, force: true }); - } -}); - test("concurrent requests for the same ID share one vendor fetch", async () => { let finish!: (report: unknown) => void; const response = new Promise((resolve) => { @@ -177,15 +115,15 @@ test("concurrent requests for the same ID share one vendor fetch", async () => { registry.register( source({ id: "coalesced", + discover: async () => [{ account: "one" }], fetch: async () => { fetches++; return response; }, }), ); - await registry.resolveReference({ source: "coalesced", input: { account: "one" } }); - const first = registry.listReports({ reportIds: ["coalesced:one"] }); - const second = registry.listReports({ reportIds: ["coalesced:one"], forceRefresh: true }); + const first = registry.listReports(); + const second = registry.listReports(); finish({ status: "available", windows: [] }); const [one, two] = await Promise.all([first, second]); expect(one[0]).toBe(two[0]); @@ -211,11 +149,13 @@ test("source failure IDs cannot collide with an account named error", async () = test("expired cached entries are pruned when a new report is written", async () => { let now = 0; const registry = new UsageSourceRegistry(() => now, 100); - registry.register(source({ id: "source" })); - await registry.resolveReference({ source: "source", input: { account: "old" } }); + let account = "old"; + registry.register(source({ id: "source", discover: async () => [{ account }] })); + await registry.listReports(); await registry.listReports({ reportIds: ["source:old"] }); now = 101; - await registry.resolveReference({ source: "source", input: { account: "new" } }); + account = "new"; + await registry.listReports(); await registry.listReports({ reportIds: ["source:new"] }); const cache = Reflect.get(registry, "cache") as Map; expect([...cache.keys()]).toEqual(["source:new"]); @@ -231,11 +171,23 @@ test("discovery failures have an ID outside the account namespace", async () => }, }), ); - expect(await registry.resolveReference({ source: "source", input: { account: "error" } })).toBe( - "source:error", - ); const discovered = await registry.listReports(); expect(discovered.map((entry) => entry.id)).toEqual(["source:!error"]); - const both = await registry.listReports({ reportIds: ["source:error", "source:!error"] }); - expect(both.map((entry) => entry.report.status)).toEqual(["available", "error"]); +}); + +test("legacy listing distinguishes labeled accounts and preserves unlabeled names", async () => { + const registry = new UsageSourceRegistry(() => 1000); + registry.register( + source({ + id: "claude", + discover: async () => [{ account: "work" }, { account: "personal" }, { account: "default" }], + identify: async (input) => { + const { account } = input as { account: string }; + return { key: account, label: account === "default" ? undefined : account }; + }, + }), + ); + expect( + (await registry.listLegacyUsage()).providers.map((provider) => provider.displayName), + ).toEqual(["claude (work)", "claude (personal)", "claude"]); }); diff --git a/packages/server/src/server/plugins/usage-sources/index.ts b/packages/server/src/server/plugins/usage-sources/index.ts index a6705114de..ef8b4bd75f 100644 --- a/packages/server/src/server/plugins/usage-sources/index.ts +++ b/packages/server/src/server/plugins/usage-sources/index.ts @@ -3,7 +3,6 @@ import { type ProviderUsage, type UsageReportEntry, } from "@getpaseo/protocol/messages"; -import type { UsageReference } from "../../agent/agent-sdk-types.js"; export interface UsageSource { id: string; @@ -43,12 +42,6 @@ export class UsageSourceRegistry { for (const key of this.cache.keys()) if (key.startsWith(`${id}:`)) this.cache.delete(key); } - async resolveReference(reference: UsageReference): Promise { - const source = this.sources.get(reference.source); - if (!source) return null; - return this.identify(source, reference.input); - } - private async identify(source: UsageSource, input: unknown): Promise { try { const account = await source.identify(input); @@ -71,9 +64,9 @@ export class UsageSourceRegistry { } async listReports( - options: { forceRefresh?: boolean; reportIds?: string[]; references?: UsageReference[] } = {}, + options: { forceRefresh?: boolean; reportIds?: string[] } = {}, ): Promise { - const ids = options.reportIds ?? (await this.discoverReportIds(options.references ?? [])); + const ids = options.reportIds ?? (await this.discoverReportIds()); return Promise.all( [...new Set(ids)] .filter((id) => this.known.has(id) || this.cache.has(id)) @@ -81,7 +74,7 @@ export class UsageSourceRegistry { ); } - private async discoverReportIds(references: UsageReference[]): Promise { + private async discoverReportIds(): Promise { const discovered = await Promise.all( [...this.sources.values()].map(async (source) => { try { @@ -97,11 +90,10 @@ export class UsageSourceRegistry { } }), ); - const live = await Promise.all(references.map((reference) => this.resolveReference(reference))); - return [...discovered.flat(), ...live.filter((id): id is string => id !== null)]; + return discovered.flat(); } - // COMPAT(providerUsageList): added in v0.9.3, remove after 2027-03-26. + // COMPAT(providerUsageList): added in v0.1.98, remove after 2027-03-26. async listLegacyUsage(): Promise<{ fetchedAt: string; providers: ProviderUsage[] }> { const reports = await this.listReports(); return { @@ -113,7 +105,9 @@ export class UsageSourceRegistry { : new Date(this.now()).toISOString(), providers: reports.map((entry) => ({ providerId: entry.sourceId, - displayName: entry.sourceLabel, + displayName: entry.account.label + ? `${entry.sourceLabel} (${entry.account.label})` + : entry.sourceLabel, status: entry.report.status, planLabel: entry.report.planLabel ?? null, windows: entry.report.windows, diff --git a/packages/server/src/server/session.ts b/packages/server/src/server/session.ts index f83040c8c9..1307cf808f 100644 --- a/packages/server/src/server/session.ts +++ b/packages/server/src/server/session.ts @@ -134,7 +134,6 @@ import { type AgentPermissionResponse, type AgentRunOptions, type AgentSessionConfig, - type UsageReference, } from "./agent/agent-sdk-types.js"; import type { StoredAgentRecord } from "./agent/agent-storage.js"; import type { AgentStorage } from "./agent/agent-storage.js"; @@ -514,9 +513,7 @@ export interface SessionOptions { listUsageReports(options?: { forceRefresh?: boolean; reportIds?: string[]; - references?: UsageReference[]; }): Promise; - resolveUsageReference(reference: UsageReference): Promise; listLegacyUsage(): Promise<{ fetchedAt: string; providers: ProviderUsage[] }>; }; orchestrationSkills?: import("./orchestration-skills/index.js").OrchestrationSkills; @@ -999,8 +996,6 @@ export class Session { }); this.usageSession = new UsageSession({ emit: (msg) => this.emit(msg), - listAgents: () => this.agentManager.listAgents(), - getAgent: (agentId) => this.agentManager.getAgent(agentId), runtime: pluginRuntime, logger: this.sessionLogger, }); @@ -3016,8 +3011,6 @@ export class Session { return this.usageSession.handleLegacyList(msg); case "usage.list_reports.request": return this.usageSession.handleListReports(msg); - case "agent.resolve_usage_report.request": - return this.usageSession.handleResolveAgentReport(msg); default: return undefined; } diff --git a/packages/server/src/server/session/usage/usage-session.test.ts b/packages/server/src/server/session/usage/usage-session.test.ts index 7914af3872..a56d6f6c9a 100644 --- a/packages/server/src/server/session/usage/usage-session.test.ts +++ b/packages/server/src/server/session/usage/usage-session.test.ts @@ -1,18 +1,11 @@ import pino from "pino"; import { expect, test } from "vitest"; -import type { AgentSession } from "../../agent/agent-sdk-types.js"; import type { SessionOutboundMessage } from "../../messages.js"; import { UsageSession } from "./usage-session.js"; -test("collects live references for usage reports and resolves an agent report", async () => { +test("lists reports from usage sources", async () => { const emitted: SessionOutboundMessage[] = []; - const references: unknown[] = []; - const reference = { source: "fixture", input: { account: "one" } }; - const agent = { - session: { - getUsageReference: async () => reference, - } as AgentSession, - }; + const requested: Array<{ forceRefresh?: boolean; reportIds?: string[] }> = []; const entry = { id: "fixture:one", account: {}, @@ -23,17 +16,11 @@ test("collects live references for usage reports and resolves an agent report", }; const usage = new UsageSession({ emit: (message) => emitted.push(message), - listAgents: () => [agent, { session: null }], - getAgent: (id) => (id === "one" ? agent : null), runtime: { async listUsageReports(options) { - references.push(options.references); + requested.push(options); return [entry]; }, - async resolveUsageReference(value) { - references.push(value); - return entry.id; - }, async listLegacyUsage() { return { fetchedAt: "2026-01-01T00:00:00.000Z", providers: [] }; }, @@ -42,15 +29,9 @@ test("collects live references for usage reports and resolves an agent report", }); await usage.handleListReports({ type: "usage.list_reports.request", requestId: "list" }); - await usage.handleResolveAgentReport({ - type: "agent.resolve_usage_report.request", - requestId: "one", - agentId: "one", - }); - expect(references).toEqual([[reference], reference]); - expect(emitted.map((message) => message.type)).toEqual([ - "usage.list_reports.response", - "agent.resolve_usage_report.response", + expect(requested).toEqual([{ forceRefresh: undefined, reportIds: undefined }]); + expect(emitted).toEqual([ + { type: "usage.list_reports.response", payload: { requestId: "list", reports: [entry] } }, ]); }); @@ -58,15 +39,10 @@ test("surfaces a legacy usage-list failure as an rpc_error envelope", async () = const emitted: SessionOutboundMessage[] = []; const usage = new UsageSession({ emit: (message) => emitted.push(message), - listAgents: () => [], - getAgent: () => null, runtime: { async listUsageReports() { return []; }, - async resolveUsageReference() { - return null; - }, async listLegacyUsage(): Promise { throw new Error("quota service down"); }, @@ -79,24 +55,3 @@ test("surfaces a legacy usage-list failure as an rpc_error envelope", async () = payload: { requestId: "u1", code: "provider_usage_list_failed" }, }); }); - -test("unknown agent returns agent_not_found", async () => { - const emitted: SessionOutboundMessage[] = []; - const usage = new UsageSession({ - emit: (message) => emitted.push(message), - listAgents: () => [], - getAgent: () => null, - runtime: { - listUsageReports: async () => [], - resolveUsageReference: async () => null, - listLegacyUsage: async () => ({ fetchedAt: "", providers: [] }), - }, - logger: pino({ level: "silent" }), - }); - await usage.handleResolveAgentReport({ - type: "agent.resolve_usage_report.request", - requestId: "missing", - agentId: "missing", - }); - expect(emitted[0]).toMatchObject({ type: "rpc_error", payload: { code: "agent_not_found" } }); -}); diff --git a/packages/server/src/server/session/usage/usage-session.ts b/packages/server/src/server/session/usage/usage-session.ts index 6665a7454f..c94eaf2fcf 100644 --- a/packages/server/src/server/session/usage/usage-session.ts +++ b/packages/server/src/server/session/usage/usage-session.ts @@ -1,23 +1,14 @@ import type pino from "pino"; import type { ProviderUsage, UsageReportEntry } from "@getpaseo/protocol/messages"; -import type { AgentSession, UsageReference } from "../../agent/agent-sdk-types.js"; import type { SessionInboundMessage, SessionOutboundMessage } from "../../messages.js"; -interface UsageAgent { - session: AgentSession | null; -} - export interface UsageSessionOptions { emit(message: SessionOutboundMessage): void; - listAgents(): UsageAgent[]; - getAgent(agentId: string): UsageAgent | null; runtime?: { listUsageReports(options: { forceRefresh?: boolean; reportIds?: string[]; - references: UsageReference[]; }): Promise; - resolveUsageReference(reference: UsageReference): Promise; listLegacyUsage(): Promise<{ fetchedAt: string; providers: ProviderUsage[] }>; }; logger: pino.Logger; @@ -31,22 +22,9 @@ export class UsageSession { ): Promise { try { if (!this.options.runtime) throw new Error("Plugin runtime is unavailable"); - const references = - msg.reportIds === undefined - ? ( - await Promise.all( - this.options - .listAgents() - .map( - async (agent) => agent.session?.getUsageReference?.().catch(() => null) ?? null, - ), - ) - ).filter((reference): reference is UsageReference => reference !== null) - : []; const reports = await this.options.runtime.listUsageReports({ forceRefresh: msg.forceRefresh, reportIds: msg.reportIds, - references, }); this.options.emit({ type: "usage.list_reports.response", @@ -57,29 +35,6 @@ export class UsageSession { } } - async handleResolveAgentReport( - msg: Extract, - ): Promise { - try { - if (!this.options.runtime) throw new Error("Plugin runtime is unavailable"); - const agent = this.options.getAgent(msg.agentId); - if (!agent) { - this.emitError(msg, new Error(`Agent not found: ${msg.agentId}`), "agent_not_found"); - return; - } - const reference = (await agent.session?.getUsageReference?.()) ?? null; - const reportId = reference - ? await this.options.runtime.resolveUsageReference(reference) - : null; - this.options.emit({ - type: "agent.resolve_usage_report.response", - payload: { requestId: msg.requestId, reportId }, - }); - } catch (error) { - this.emitError(msg, error, "agent_resolve_usage_report_failed"); - } - } - // COMPAT(providerUsageList): added in v0.9.3, remove after 2027-03-26. async handleLegacyList( msg: Extract, @@ -111,10 +66,7 @@ export class UsageSession { } private emitError( - msg: Extract< - SessionInboundMessage, - { type: "usage.list_reports.request" | "agent.resolve_usage_report.request" } - >, + msg: Extract, error: unknown, code: string, ): void { diff --git a/plugin-examples/hosts/index.client.tsx b/plugin-examples/hosts/index.client.tsx index f6b50a0a08..76480eac00 100644 --- a/plugin-examples/hosts/index.client.tsx +++ b/plugin-examples/hosts/index.client.tsx @@ -1,8 +1,15 @@ -import type { PluginClientContext } from "@getpaseo/plugin/client"; +import { useCallback } from "react"; +import type { PluginClientContext, PluginSidebarItemProps } from "@getpaseo/plugin/client"; +import { SidebarRow } from "@getpaseo/plugin/client/ui"; import { Hosts } from "./client/hosts"; +function HostsItem({ currentScreen, openScreen }: PluginSidebarItemProps) { + const open = useCallback(() => openScreen({ screenId: "main" }), [openScreen]); + return ; +} + export default function contribute(client: PluginClientContext) { - client.addSurface("main", Hosts); - client.addSidebarItem({ id: "hosts", title: "Host agents", icon: "Server", surface: "main" }); + client.addScreen({ id: "main", title: "Host agents", Component: Hosts }); + client.addSidebarHeaderItem({ id: "hosts", title: "Host agents", Component: HostsItem }); return () => {}; } diff --git a/plugin-examples/modal-ui/index.client.tsx b/plugin-examples/modal-ui/index.client.tsx index e7b2baf54e..287d3bdffa 100644 --- a/plugin-examples/modal-ui/index.client.tsx +++ b/plugin-examples/modal-ui/index.client.tsx @@ -1,13 +1,21 @@ -import type { PluginClientContext } from "@getpaseo/plugin/client"; +import { useCallback } from "react"; +import type { PluginClientContext, PluginSidebarItemProps } from "@getpaseo/plugin/client"; +import { SidebarRow } from "@getpaseo/plugin/client/ui"; import { ModalExamples } from "./client/examples"; +function ModalExamplesItem({ currentScreen, openScreen }: PluginSidebarItemProps) { + const open = useCallback(() => openScreen({ screenId: "main" }), [openScreen]); + return ( + + ); +} + export default function contribute(plugin: PluginClientContext) { - plugin.addSurface("main", ModalExamples); - plugin.addSidebarItem({ + plugin.addScreen({ id: "main", title: "Modal examples", Component: ModalExamples }); + plugin.addSidebarHeaderItem({ id: "main", title: "Modal examples", - icon: "PanelsTopLeft", - surface: "main", + Component: ModalExamplesItem, }); plugin.addWorkspacePanel({ id: "examples", diff --git a/plugins/claude-usage-source/server/keychain.test.ts b/plugins/claude-usage-source/server/keychain.test.ts index b4d5295185..710e3b9cb4 100644 --- a/plugins/claude-usage-source/server/keychain.test.ts +++ b/plugins/claude-usage-source/server/keychain.test.ts @@ -98,26 +98,21 @@ function fixtureFetch(expectedToken: string): typeof fetch { }) as unknown as typeof fetch; } -describe("Claude usage input forms", () => { - it("reads only the selected configDir file", async () => { +describe("Claude default credential lookup", () => { + it("reads the configured Claude home file", async () => { const dir = mkdtempSync(join(tmpdir(), "claude-usage-input-")); try { writeFileSync( join(dir, ".credentials.json"), JSON.stringify({ claudeAiOauth: { accessToken: "fixture-file" } }), ); - expect((await fetchUsage({ configDir: dir }, fixtureFetch("fixture-file"))).status).toBe( + expect((await fetchUsage({}, fixtureFetch("fixture-file"), { claudeHome: dir })).status).toBe( "available", ); } finally { rmSync(dir, { recursive: true, force: true }); } }); - it("uses an accessToken without reading a file", async () => { - expect( - (await fetchUsage({ accessToken: "fixture-direct" }, fixtureFetch("fixture-direct"))).status, - ).toBe("available"); - }); it("default discovery reads the credential file", async () => { const dir = mkdtempSync(join(tmpdir(), "claude-usage-default-")); try { @@ -131,11 +126,13 @@ describe("Claude usage input forms", () => { rmSync(dir, { recursive: true, force: true }); } }); - it("stays unavailable when non-default configDir has no credentials", async () => { + it("stays unavailable when the configured home has no credentials", async () => { const dir = mkdtempSync(join(tmpdir(), "claude-empty-config-")); try { const fetchApi = vi.fn() as unknown as typeof fetch; - expect((await fetchUsage({ configDir: dir }, fetchApi)).status).toBe("unavailable"); + expect((await fetchUsage({}, fetchApi, { claudeHome: dir, platform: "linux" })).status).toBe( + "unavailable", + ); expect(fetchApi).not.toHaveBeenCalled(); } finally { rmSync(dir, { recursive: true, force: true }); @@ -151,12 +148,14 @@ describe("Claude Keychain token usage", () => { }), ); const credentials = await readClaudeKeychainCredentials(run, "fixture-account"); - const token = (credentials as { claudeAiOauth: { accessToken: string } }).claudeAiOauth - .accessToken; const fetchApi = vi.fn( async () => new Response(null, { status: 401 }), ) as unknown as typeof fetch; - const report = await fetchUsage({ accessToken: token }, fetchApi); + const report = await fetchUsage({}, fetchApi, { + platform: "darwin", + claudeHome: "/nonexistent-paseo-test-home", + readKeychainCredentials: async () => credentials, + }); expect(report.status).toBe("unavailable"); expect(fetchApi).toHaveBeenCalledTimes(1); expect(run).toHaveBeenCalledTimes(1); diff --git a/plugins/claude-usage-source/server/usage.test.ts b/plugins/claude-usage-source/server/usage.test.ts index 421d0abd8a..d20162e93d 100644 --- a/plugins/claude-usage-source/server/usage.test.ts +++ b/plugins/claude-usage-source/server/usage.test.ts @@ -3,6 +3,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { fetchUsage } from "./usage.js"; +import { inputSchema } from "../shared/input.js"; import type { UsageReport } from "@getpaseo/plugin/server/usage"; function writeClaudeCredentials( @@ -615,7 +616,7 @@ it("identify reads account and organization from the selected Claude config", as }, }), ); - expect(await identify({ configDir: directory })).toEqual({ + expect(await identify({}, undefined, undefined, { accountHome: directory })).toEqual({ key: "account-uuid.org-uuid", label: "test@example.com", }); @@ -624,7 +625,7 @@ it("identify reads account and organization from the selected Claude config", as } }); -it("token-only Claude inputs use a cached OAuth profile identity", async () => { +it("Claude credentials without account metadata use a cached OAuth profile identity", async () => { const { identify } = await import("./usage.js"); let calls = 0; const fetchProfile: typeof fetch = async () => { @@ -637,37 +638,46 @@ it("token-only Claude inputs use a cached OAuth profile identity", async () => { { status: 200 }, ); }; - expect(await identify({ accessToken: "fixture-claude-token" }, fetchProfile)).toEqual({ + expect(await identify({}, fetchProfile, Date.now, tokenLookup("fixture-claude-token"))).toEqual({ key: "account-uuid.org-uuid", label: "test@example.com", }); - expect(await identify({ accessToken: "fixture-claude-token" }, fetchProfile)).toEqual({ + expect(await identify({}, fetchProfile, Date.now, tokenLookup("fixture-claude-token"))).toEqual({ key: "account-uuid.org-uuid", label: "test@example.com", }); expect(calls).toBe(1); }); -it("identifies the same configDir credentials that fetch uses when oauthAccount is absent", async () => { +it("identifies the same default credentials that fetch uses when oauthAccount is absent", async () => { const { identify } = await import("./usage.js"); const directory = mkdtempSync(join(tmpdir(), "claude-credential-identity-")); try { writeClaudeCredentials(directory, "fixture-credential-token"); writeFileSync(join(directory, ".claude.json"), JSON.stringify({})); let usageRequested = false; - await fetchUsage({ configDir: directory }, async () => { - usageRequested = true; - return new Response(null, { status: 401 }); - }); + await fetchUsage( + {}, + async () => { + usageRequested = true; + return new Response(null, { status: 401 }); + }, + { claudeHome: directory }, + ); expect(usageRequested).toBe(true); let profileRequested = false; - const account = await identify({ configDir: directory }, async () => { - profileRequested = true; - return jsonResponse({ - account: { uuid: "account-uuid", email: "owner@example.test" }, - organization: { uuid: "org-uuid" }, - }); - }); + const account = await identify( + {}, + async () => { + profileRequested = true; + return jsonResponse({ + account: { uuid: "account-uuid", email: "owner@example.test" }, + organization: { uuid: "org-uuid" }, + }); + }, + Date.now, + { claudeHome: directory, accountHome: directory }, + ); expect(profileRequested).toBe(true); expect(account).toEqual({ key: "account-uuid.org-uuid", label: "owner@example.test" }); } finally { @@ -683,12 +693,13 @@ it("expires cached token profiles after five minutes", async () => { calls++; return jsonResponse({ account: { uuid: "account-uuid" }, organization: { uuid: "org-uuid" } }); }; - const input = { accessToken: "ttl-fixture-token" }; - await identify(input, fetchProfile, () => now); - await identify(input, fetchProfile, () => now); + const input = {}; + const lookup = tokenLookup("ttl-fixture-token"); + await identify(input, fetchProfile, () => now, lookup); + await identify(input, fetchProfile, () => now, lookup); expect(calls).toBe(1); now = 300_001; - await identify(input, fetchProfile, () => now); + await identify(input, fetchProfile, () => now, lookup); expect(calls).toBe(2); }); @@ -770,8 +781,26 @@ it("bounds cached profiles across many token rotations", async () => { return jsonResponse({ account: { uuid: "account" }, organization: { uuid: "org" } }); }; for (let index = 0; index < 129; index++) { - await identify({ accessToken: `rotation-fixture-${index}` }, fetchProfile); + await identify({}, fetchProfile, Date.now, tokenLookup(`rotation-fixture-${index}`)); } - await identify({ accessToken: "rotation-fixture-0" }, fetchProfile); + await identify({}, fetchProfile, Date.now, tokenLookup("rotation-fixture-0")); expect(calls).toBe(130); }); + +it("discovery input rejects explicit credential routes", () => { + expect(inputSchema.safeParse({ accessToken: "unused" }).success).toBe(false); + expect(inputSchema.safeParse({ configDir: "/unused" }).success).toBe(false); + expect(inputSchema.parse({})).toEqual({}); +}); + +const tokenDirectories: string[] = []; +afterEach(() => { + for (const directory of tokenDirectories.splice(0)) + rmSync(directory, { recursive: true, force: true }); +}); +function tokenLookup(accessToken: string) { + const directory = mkdtempSync(join(tmpdir(), "claude-profile-token-")); + tokenDirectories.push(directory); + writeClaudeCredentials(directory, accessToken); + return { claudeHome: directory, accountHome: directory }; +} diff --git a/plugins/claude-usage-source/server/usage.ts b/plugins/claude-usage-source/server/usage.ts index 48ac928d16..9e280df0e5 100644 --- a/plugins/claude-usage-source/server/usage.ts +++ b/plugins/claude-usage-source/server/usage.ts @@ -336,11 +336,9 @@ interface ClaudeCredentialLookup { /** Shared credential lookup for usage fetches and account identification. */ export async function resolveClaudeCredentials( - input: UsageInput, + _input: UsageInput, lookup: ClaudeCredentialLookup = {}, ): Promise { - if ("accessToken" in input) return { oauth: { accessToken: input.accessToken } }; - if ("configDir" in input) return readCredentialFile(join(input.configDir, ".credentials.json")); const claudeHome = lookup.claudeHome ?? process.env["CLAUDE_HOME"] ?? join(homedir(), ".claude"); const fileCredentials = await readCredentialFile(join(claudeHome, ".credentials.json")); if (fileCredentials) return fileCredentials; @@ -433,7 +431,6 @@ export async function fetchUsage( scopedLimitsFromResponse(resp.limits), ); const windows = [...unscopedWindows(resp), ...scopedWindows(scoped)]; - if (windows[0]) windows[0].headline = true; if (windows.length === 0) { // The response parsed but described nothing. That silence is how the previous @@ -484,21 +481,18 @@ export async function identify( now: () => number = Date.now, credentialLookup: ClaudeCredentialLookup = {}, ) { - const directory = - "configDir" in input ? input.configDir : (credentialLookup.accountHome ?? homedir()); - if (!("accessToken" in input)) { - try { - const config = z - .object({ oauthAccount: ClaudeAccountSchema }) - .parse(JSON.parse(await fs.readFile(join(directory, ".claude.json"), "utf8"))); - const account = config.oauthAccount; - return { - key: `${account.accountUuid}.${account.organizationUuid}`, - ...(account.emailAddress ? { label: account.emailAddress } : {}), - }; - } catch { - // Credentials may still be present even when account metadata is absent. - } + const directory = credentialLookup.accountHome ?? homedir(); + try { + const config = z + .object({ oauthAccount: ClaudeAccountSchema }) + .parse(JSON.parse(await fs.readFile(join(directory, ".claude.json"), "utf8"))); + const account = config.oauthAccount; + return { + key: `${account.accountUuid}.${account.organizationUuid}`, + ...(account.emailAddress ? { label: account.emailAddress } : {}), + }; + } catch { + // Credentials may still be present even when account metadata is absent. } const credentials = await resolveClaudeCredentials(input, credentialLookup); if (!credentials) return null; diff --git a/plugins/claude-usage-source/shared/input.ts b/plugins/claude-usage-source/shared/input.ts index 3668dd668f..9d9a5f432e 100644 --- a/plugins/claude-usage-source/shared/input.ts +++ b/plugins/claude-usage-source/shared/input.ts @@ -1,7 +1,3 @@ import { z } from "zod"; -export const inputSchema = z.union([ - z.object({ configDir: z.string().min(1) }).strict(), - z.object({ accessToken: z.string().min(1) }).strict(), - z.object({}).strict(), -]); +export const inputSchema = z.object({}).strict(); export type UsageInput = z.infer; diff --git a/plugins/codex-usage-source/server/usage.test.ts b/plugins/codex-usage-source/server/usage.test.ts index e45291bfbd..dfa58ef603 100644 --- a/plugins/codex-usage-source/server/usage.test.ts +++ b/plugins/codex-usage-source/server/usage.test.ts @@ -1,11 +1,28 @@ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { afterEach, expect, test } from "vitest"; +import { afterEach, beforeEach, expect, test } from "vitest"; import { fetchUsage, identify } from "./usage.js"; +import { inputSchema } from "../shared/input.js"; + +let fixtureHome: string; +beforeEach(async () => { + fixtureHome = await mkdtemp(join(tmpdir(), "usage-codex-default-")); + process.env["CODEX_HOME"] = fixtureHome; + await writeAuth("fixture-supplied"); +}); + +async function writeAuth(accessToken: string, accountId?: string) { + await writeFile( + join(fixtureHome, "auth.json"), + JSON.stringify({ tokens: { access_token: accessToken, account_id: accountId } }), + ); +} + const originalHome = process.env["CODEX_HOME"]; -afterEach(() => { +afterEach(async () => { + await rm(fixtureHome, { recursive: true, force: true }); if (originalHome === undefined) delete process.env["CODEX_HOME"]; else process.env["CODEX_HOME"] = originalHome; }); @@ -41,42 +58,33 @@ test("default input reads CODEX_HOME auth and preserves the usage request", asyn expect(report).toMatchObject({ status: "available", planLabel: "plus", - windows: [{ id: "session", usedPct: 30, headline: true }], + windows: [{ id: "session", usedPct: 30 }], }); } finally { await rm(home, { recursive: true, force: true }); } }); -test("explicit codexHome reads only that auth file", async () => { - const home = await mkdtemp(join(tmpdir(), "usage-codex-")); - try { - await writeFile( - join(home, "auth.json"), - JSON.stringify({ tokens: { access_token: "fixture-home", account_id: "account-home" } }), - ); - await fetchUsage({ codexHome: home }, (_url, init) => - response(init?.headers ?? {}, "account-home"), - ); - expect(await identify({ codexHome: home })).toEqual({ key: "account-home" }); - } finally { - await rm(home, { recursive: true, force: true }); +test("discovery input rejects explicit credential routes", () => { + for (const input of [ + { codexHome: "/unused" }, + { accessToken: "unused" }, + { accessToken: "unused", accountId: "unused" }, + ]) { + expect(inputSchema.safeParse(input).success).toBe(false); } + expect(inputSchema.parse({})).toEqual({}); }); test("identify returns a key when fetch finds Codex token credentials", async () => { - await fetchUsage( - { accessToken: "fixture-supplied", accountId: "account-supplied" }, - (_url, init) => response(init?.headers ?? {}, "account-supplied"), - ); - expect( - await identify({ accessToken: "fixture-supplied", accountId: "account-supplied" }), - ).toEqual({ key: "account-supplied" }); + await writeAuth("fixture-supplied", "account-supplied"); + await fetchUsage({}, (_url, init) => response(init?.headers ?? {}, "account-supplied")); + expect(await identify({})).toEqual({ key: "account-supplied" }); }); test("coerces credit balance and marks a 96 percent window dangerous", async () => { const report = await fetchUsage( - { accessToken: "fixture-supplied" }, + {}, async () => new Response( JSON.stringify({ @@ -100,7 +108,7 @@ test("coerces credit balance and marks a 96 percent window dangerous", async () test("HTML usage body is unavailable", async () => { const report = await fetchUsage( - { accessToken: "fixture-supplied" }, + {}, async () => new Response("Login", { status: 200 }), ); expect(report.status).toBe("unavailable"); @@ -109,6 +117,7 @@ test("HTML usage body is unavailable", async () => { test("401 leaves auth.json byte for byte unchanged and makes no refresh request", async () => { const home = await mkdtemp(join(tmpdir(), "usage-codex-")); try { + process.env["CODEX_HOME"] = home; const authPath = join(home, "auth.json"); const before = JSON.stringify({ OPENAI_API_KEY: null, @@ -122,7 +131,7 @@ test("401 leaves auth.json byte for byte unchanged and makes no refresh request" }); await writeFile(authPath, before); let calls = 0; - const report = await fetchUsage({ codexHome: home }, async (url) => { + const report = await fetchUsage({}, async (url) => { expect(String(url)).toBe("https://chatgpt.com/backend-api/wham/usage"); calls++; return new Response(null, { status: 401 }); @@ -135,17 +144,21 @@ test("401 leaves auth.json byte for byte unchanged and makes no refresh request" } }); -test("token-only account claim survives token rotation", async () => { +test("default auth account claim survives token rotation", async () => { const token = (suffix: string) => `header.${Buffer.from(JSON.stringify({ "https://api.openai.com/auth": { chatgpt_account_id: "work-id" }, suffix })).toString("base64url")}.signature`; - expect(await identify({ accessToken: token("first") })).toEqual({ key: "work-id" }); - expect(await identify({ accessToken: token("second") })).toEqual({ key: "work-id" }); - expect(await identify({ accessToken: "opaque-token" })).toBeNull(); + await writeAuth(token("first")); + expect(await identify({})).toEqual({ key: "work-id" }); + await writeAuth(token("second")); + expect(await identify({})).toEqual({ key: "work-id" }); + await writeAuth("opaque-token"); + expect(await identify({})).toBeNull(); }); test("identify reads an email label from auth id_token when access token has no profile", async () => { const home = await mkdtemp(join(tmpdir(), "usage-codex-label-")); try { + process.env["CODEX_HOME"] = home; const idToken = `header.${Buffer.from(JSON.stringify({ email: "id-owner@example.test" })).toString("base64url")}.signature`; await writeFile( join(home, "auth.json"), @@ -153,7 +166,7 @@ test("identify reads an email label from auth id_token when access token has no tokens: { account_id: "account-id", access_token: "opaque-token", id_token: idToken }, }), ); - expect(await identify({ codexHome: home })).toEqual({ + expect(await identify({})).toEqual({ key: "account-id", label: "id-owner@example.test", }); diff --git a/plugins/codex-usage-source/server/usage.ts b/plugins/codex-usage-source/server/usage.ts index c7b07ce8f3..3fb660ae09 100644 --- a/plugins/codex-usage-source/server/usage.ts +++ b/plugins/codex-usage-source/server/usage.ts @@ -33,17 +33,13 @@ const responseSchema = z.object({ }); export async function readAuth( - input: CodexUsageInput, + _input: CodexUsageInput, ): Promise<{ token: string; accountId?: string; idToken?: string } | null> { - if ("accessToken" in input) return { token: input.accessToken, accountId: input.accountId }; - const candidates = - "codexHome" in input - ? [join(input.codexHome, "auth.json")] - : [ - ...(process.env["CODEX_HOME"] ? [join(process.env["CODEX_HOME"], "auth.json")] : []), - join(homedir(), ".config", "codex", "auth.json"), - join(homedir(), ".codex", "auth.json"), - ]; + const candidates = [ + ...(process.env["CODEX_HOME"] ? [join(process.env["CODEX_HOME"], "auth.json")] : []), + join(homedir(), ".config", "codex", "auth.json"), + join(homedir(), ".codex", "auth.json"), + ]; for (const path of candidates) { try { const auth = authSchema.parse(JSON.parse(await readFile(path, "utf8"))); @@ -64,7 +60,6 @@ function usageWindow( id: string, label: string, value: z.infer | null | undefined, - headline = false, ): UsageWindow | null { if (!value) return null; const usedPct = value.used_percent ?? 0; @@ -74,7 +69,6 @@ function usageWindow( utilizationPct: usedPct, resetsAt: value.reset_at != null ? new Date(value.reset_at * 1000).toISOString() : null, tone: toneFromUsedPct(usedPct), - headline, }); } @@ -101,7 +95,7 @@ export async function fetchUsage( if (text.trim().startsWith("<")) return { status: "unavailable", windows: [] }; const usage = responseSchema.parse(JSON.parse(text)); const windows = [ - usageWindow("session", "Session", usage.rate_limit?.primary_window, true), + usageWindow("session", "Session", usage.rate_limit?.primary_window), usageWindow("weekly", "Weekly", usage.rate_limit?.secondary_window), usageWindow("code_review", "Code review", usage.code_review_rate_limit?.primary_window), ].filter((window): window is UsageWindow => window !== null); diff --git a/plugins/codex-usage-source/shared/input.ts b/plugins/codex-usage-source/shared/input.ts index 0c54b51e69..baa8ee7ca6 100644 --- a/plugins/codex-usage-source/shared/input.ts +++ b/plugins/codex-usage-source/shared/input.ts @@ -1,8 +1,4 @@ import { z } from "zod"; -export const inputSchema = z.union([ - z.object({ codexHome: z.string().min(1) }).strict(), - z.object({ accessToken: z.string().min(1), accountId: z.string().optional() }).strict(), - z.object({}).strict(), -]); +export const inputSchema = z.object({}).strict(); export type CodexUsageInput = z.infer; diff --git a/plugins/grok-usage-source/server/usage.ts b/plugins/grok-usage-source/server/usage.ts index 251674f597..ba4ab829cf 100644 --- a/plugins/grok-usage-source/server/usage.ts +++ b/plugins/grok-usage-source/server/usage.ts @@ -144,7 +144,6 @@ export async function fetchUsage( const resp = GrokUsageResponseSchema.parse(await res.json()); const balance = grokMonthlyCreditBalance(resp); const window = grokUsageWindow(resp); - if (window) window.headline = true; return { status: "available", diff --git a/plugins/kimi-usage-source/server/usage.ts b/plugins/kimi-usage-source/server/usage.ts index bd20560ae7..c590b1c984 100644 --- a/plugins/kimi-usage-source/server/usage.ts +++ b/plugins/kimi-usage-source/server/usage.ts @@ -312,7 +312,6 @@ export async function fetchUsage( } const windows = kimiUsageWindowsFromPayload(await res.json()); - if (windows[0]) windows[0].headline = true; return { status: "available", diff --git a/plugins/minimax-usage-source/server/usage.ts b/plugins/minimax-usage-source/server/usage.ts index 9c0b019e0f..4ae802a285 100644 --- a/plugins/minimax-usage-source/server/usage.ts +++ b/plugins/minimax-usage-source/server/usage.ts @@ -255,7 +255,6 @@ export async function fetchUsage( const weeklyWindow = toWeeklyWindow(name, model); if (weeklyWindow) windows.push(weeklyWindow); } - if (windows[0]) windows[0].headline = true; return { status: windows.length > 0 ? "available" : "unavailable", diff --git a/plugins/opencode-go-usage-source/server/usage.test.ts b/plugins/opencode-go-usage-source/server/usage.test.ts index a9120cbebe..c475d96b36 100644 --- a/plugins/opencode-go-usage-source/server/usage.test.ts +++ b/plugins/opencode-go-usage-source/server/usage.test.ts @@ -1,9 +1,30 @@ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { expect, test } from "vitest"; +import { afterEach, beforeEach, expect, test } from "vitest"; import { discover, fetchUsage, identify } from "./usage.js"; +import { inputSchema } from "../shared/input.js"; + +let fixtureDirectory: string; +let fixturePath: string; +beforeEach(async () => { + fixtureDirectory = await mkdtemp(join(tmpdir(), "go-default-")); + fixturePath = join(fixtureDirectory, "auth.json"); + await writeFile( + fixturePath, + JSON.stringify({ "opencode-go": { type: "api", key: "fixture-secret" } }), + ); +}); +afterEach(async () => { + await rm(fixtureDirectory, { recursive: true, force: true }); +}); + +test("discovery input rejects an explicit API key", () => { + expect(inputSchema.safeParse({ apiKey: "unused" }).success).toBe(false); + expect(inputSchema.parse({})).toEqual({}); +}); + const upstreamResponse = { usage: { rolling: { status: "ok", percent: 21, resetsAt: "2026-09-26T20:00:00.000Z" }, @@ -33,12 +54,10 @@ test("discovers and fetches the default key from read-only auth.json", async () { url: "https://opencode.ai/zen/go/v1/usage", authorization: "Bearer fixture-key" }, ]); expect(report.status).toBe("available"); - expect( - report.windows.map((window) => [window.id, window.usedPct, window.resetsAt, window.headline]), - ).toEqual([ - ["rolling", 21, "2026-09-26T20:00:00.000Z", true], - ["weekly", 42, "2026-09-28T00:00:00.000Z", undefined], - ["monthly", 100, "2026-10-01T00:00:00.000Z", undefined], + expect(report.windows.map((window) => [window.id, window.usedPct, window.resetsAt])).toEqual([ + ["rolling", 21, "2026-09-26T20:00:00.000Z"], + ["weekly", 42, "2026-09-28T00:00:00.000Z"], + ["monthly", 100, "2026-10-01T00:00:00.000Z"], ]); // The current upstream endpoint returns windows only; do not invent balances. expect(report.balances).toBeUndefined(); @@ -68,23 +87,24 @@ test("omits default discovery when auth.json lacks an OpenCode Go API key", asyn }); test.each([401, 403])("maps HTTP %i to unavailable", async (status) => { - const report = await fetchUsage( - { apiKey: "fixture-key" }, - async () => new Response(null, { status }), - ); + const report = await fetchUsage({}, async () => new Response(null, { status }), fixturePath); expect(report.status).toBe("unavailable"); expect(report.windows).toEqual([]); }); test("identify returns a key when fetch finds OpenCode Go credentials", async () => { let requested = false; - await fetchUsage({ apiKey: "fixture-secret" }, async () => { - requested = true; - return new Response(null, { status: 401 }); - }); + await fetchUsage( + {}, + async () => { + requested = true; + return new Response(null, { status: 401 }); + }, + fixturePath, + ); expect(requested).toBe(true); - const identity = await identify({ apiKey: "fixture-secret" }); + const identity = await identify({}, fixturePath); expect(identity?.key).toMatch(/^[a-f0-9]{64}$/); expect(identity?.key).not.toBe("fixture-secret"); - expect(await identify({ apiKey: "fixture-secret" })).toEqual(identity); + expect(await identify({}, fixturePath)).toEqual(identity); }); diff --git a/plugins/opencode-go-usage-source/server/usage.ts b/plugins/opencode-go-usage-source/server/usage.ts index 0a761fdb99..dd9944544e 100644 --- a/plugins/opencode-go-usage-source/server/usage.ts +++ b/plugins/opencode-go-usage-source/server/usage.ts @@ -49,11 +49,11 @@ export async function discover(path = authPath()): Promise> { } export async function fetchUsage( - input: Input, + _input: Input, fetchApi: typeof fetch = fetch, path = authPath(), ): Promise { - const apiKey = "apiKey" in input ? input.apiKey : await readDefaultKey(path); + const apiKey = await readDefaultKey(path); if (!apiKey) return { status: "unavailable", windows: [] }; const response = await fetchApi("https://opencode.ai/zen/go/v1/usage", { headers: { Authorization: `Bearer ${apiKey}`, Accept: "application/json" }, @@ -76,13 +76,12 @@ export async function fetchUsage( utilizationPct: value.percent, resetsAt: value.resetsAt, tone: toneFromUsedPct(value.percent), - headline: id === "rolling", }), ); return { status: "available", planLabel: "Go", windows }; } -export async function identify(input: Input, path = authPath()) { - const key = "apiKey" in input ? input.apiKey : await readDefaultKey(path); +export async function identify(_input: Input, path = authPath()) { + const key = await readDefaultKey(path); return key ? { key: hashAccountKey(key) } : null; } diff --git a/plugins/opencode-go-usage-source/shared/input.ts b/plugins/opencode-go-usage-source/shared/input.ts index 64fb3bd25b..9ba7acfb94 100644 --- a/plugins/opencode-go-usage-source/shared/input.ts +++ b/plugins/opencode-go-usage-source/shared/input.ts @@ -1,7 +1,4 @@ import { z } from "zod"; -export const inputSchema = z.union([ - z.object({ apiKey: z.string().min(1) }).strict(), - z.object({}).strict(), -]); +export const inputSchema = z.object({}).strict(); export type Input = z.infer; diff --git a/public-docs/plugins/index.md b/public-docs/plugins/index.md index fa0b717293..033235b7f0 100644 --- a/public-docs/plugins/index.md +++ b/public-docs/plugins/index.md @@ -9,7 +9,7 @@ category: Plugins # Plugin quickstart A plugin is a TypeScript project installed into one Paseo daemon. It can add -[surfaces and sidebar items](/docs/plugins/reference#surfaces-and-sidebar-items), +[screens and sidebar items](/docs/plugins/reference#screens-and-sidebar-items), [workspace panels](/docs/plugins/reference#workspace-panels), [Command Center items](/docs/plugins/reference#command-center-items), [slash commands](/docs/plugins/reference#slash-commands), @@ -37,7 +37,7 @@ npm install development dependencies for typechecking and tests only; Paseo supplies the plugin SDK, React, React Native, TanStack Query, and Zod at runtime. -The scaffold is a working plugin: a sidebar surface with a button that asks the daemon for a +The scaffold is a working plugin: a screen, opened from the sidebar, with a button that asks the daemon for a greeting through an RPC. ```text @@ -45,7 +45,7 @@ workspace-plugin/ paseo-plugin.json # plugin ID and supported Paseo versions index.client.tsx # runs in the Paseo app index.server.ts # runs in a daemon subprocess - client/greeting.tsx # the surface component + client/greeting.tsx # the screen component client/web.ts # the only file allowed to touch browser APIs server/greeting.ts # the RPC handler shared/greeting.ts # the RPC contract, imported by both @@ -54,20 +54,26 @@ workspace-plugin/ ``` Each entry default-exports one function that registers contributions and returns a cleanup -function. `index.client.tsx` registers the surface and the sidebar item that opens it: +function. `index.client.tsx` registers the screen and the sidebar item that opens it: ```tsx -import type { PluginClientContext } from "@getpaseo/plugin/client"; -import { GreetingSurface } from "./client/greeting"; +import type { PluginClientContext, PluginSidebarItemProps } from "@getpaseo/plugin/client"; +import { SidebarRow } from "@getpaseo/plugin/client/ui"; +import { GreetingScreen } from "./client/greeting"; + +function GreetingItem({ currentScreen, openScreen }: PluginSidebarItemProps) { + return ( + openScreen({ screenId: "greeting" })} + /> + ); +} export default function contribute(client: PluginClientContext) { - client.addSurface("greeting", GreetingSurface); - client.addSidebarItem({ - id: "greeting", - title: "Greeting", - icon: "MessageCircle", - surface: "greeting", - }); + client.addScreen({ id: "greeting", title: "Greeting", Component: GreetingScreen }); + client.addSidebarHeaderItem({ id: "greeting", title: "Greeting", Component: GreetingItem }); return () => {}; } ``` diff --git a/public-docs/plugins/migration.md b/public-docs/plugins/migration.md index eeb5a4cc80..7b24b37099 100644 --- a/public-docs/plugins/migration.md +++ b/public-docs/plugins/migration.md @@ -67,24 +67,29 @@ is a compile error. Use this table as the complete registration checklist. -| Old registration and location | New registration and location | -| --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | -| `plugin.handle(contract, handler)` in the old root entry | `server.handle(contract, handler)` in `index.server.ts` | -| `plugin.addSurface(id, Component)` in the old root entry | `client.addSurface(id, Component)` in `index.client.tsx` | -| `plugin.addSidebarItem(item)` in the old root entry | `client.addSidebarItem(item)` in `index.client.tsx` | -| `plugin.addWorkspacePanel(panel)` in the old root entry | `client.addWorkspacePanel(panel)` in `index.client.tsx` | -| `plugin.addCommandCenterItem(item)` in the old root entry | `client.addCommandCenterItem(item)` in `index.client.tsx` | -| `plugin.addClientSlashCommand(command)` in the old root entry | `client.addSlashCommand(command)` in `index.client.tsx` | -| `plugin.addClientSide(fn)` in the old root entry | Delete the wrapper and move the body of `fn` into the default client entry function | -| `client.addComposerPill(pill)` inside the old client callback | `client.addComposerPill(pill)` inside `index.client.tsx` or an imported `client/` function | -| New header contribution | `client.addHeaderButton({ id, workspaceId, button })` | -| `plugin.addAttachmentSource(source)` in the old root entry | `client.addAttachmentSource(source)` in `index.client.tsx` | -| New settings screen contribution | `client.addSettingsScreen(screen)` in `index.client.tsx`; see [settings screens](reference#settings-screens) | -| `plugin.addTheme(theme)` in the old root entry | `client.addTheme(theme)` in `index.client.tsx` | -| `plugin.addTimelineTransformer(transformer)` in the old root entry | `client.addTimelineTransformer(transformer)` in `index.client.tsx` | -| `plugin.addTimelineRenderer(renderer)` in the old root entry | `client.addTimelineRenderer(renderer)` in `index.client.tsx` | -| `import { defineRpc, defineAttachmentSource } from "@getpaseo/plugin/server"` in shared files | `import { defineRpc, defineAttachmentSource } from "@getpaseo/plugin"` | -| `ZodOutput` handler parameter types | `RpcInput` from `@getpaseo/plugin`; `RpcOutput` for return types | +| Old registration and location | New registration and location | +| --------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | +| `plugin.handle(contract, handler)` in the old root entry | `server.handle(contract, handler)` in `index.server.ts` | +| `plugin.addSurface(id, Component)` in the old root entry | `client.addScreen({ id, title, Component })` in `index.client.tsx` | +| `plugin.addSidebarItem({ id, title, icon, surface })` in the old root entry | `client.addSidebarHeaderItem({ id, title, Component })` in `index.client.tsx`; see [sidebar items](reference#sidebar-items) | +| New sidebar footer contribution | `client.addSidebarFooterItem({ id, title, Component })` in `index.client.tsx` | +| `plugin.addWorkspacePanel(panel)` in the old root entry | `client.addWorkspacePanel(panel)` in `index.client.tsx` | +| `plugin.addCommandCenterItem(item)` in the old root entry | `client.addCommandCenterItem(item)` in `index.client.tsx` | +| `plugin.addClientSlashCommand(command)` in the old root entry | `client.addSlashCommand(command)` in `index.client.tsx` | +| `plugin.addClientSide(fn)` in the old root entry | Delete the wrapper and move the body of `fn` into the default client entry function | +| `client.addComposerPill(pill)` inside the old client callback | `client.addComposerPill(pill)` inside `index.client.tsx` or an imported `client/` function | +| New header contribution | `client.addHeaderButton({ id, workspaceId, button })` | +| `plugin.addAttachmentSource(source)` in the old root entry | `client.addAttachmentSource(source)` in `index.client.tsx` | +| New settings screen contribution | `client.addSettingsScreen(screen)` in `index.client.tsx`; see [settings screens](reference#settings-screens) | +| `plugin.addTheme(theme)` in the old root entry | `client.addTheme(theme)` in `index.client.tsx` | +| `plugin.addTimelineTransformer(transformer)` in the old root entry | `client.addTimelineTransformer(transformer)` in `index.client.tsx` | +| `plugin.addTimelineRenderer(renderer)` in the old root entry | `client.addTimelineRenderer(renderer)` in `index.client.tsx` | +| `import { defineRpc, defineAttachmentSource } from "@getpaseo/plugin/server"` in shared files | `import { defineRpc, defineAttachmentSource } from "@getpaseo/plugin"` | +| `ZodOutput` handler parameter types | `RpcInput` from `@getpaseo/plugin`; `RpcOutput` for return types | + +Register the screen that replaces an `addSidebarItem` under that item's `id` to keep saved links +working. A `/plugin//sidebar/` link, and the older `/plugin//`, opens the +`addSidebarItem` with that `id`, or else the screen with that `id`. Import `PluginClientContext` from `@getpaseo/plugin/client` and `PluginServerContext` from `@getpaseo/plugin/server`. Remove imports of the old context type. Client registrations return idempotent removal functions, except header buttons and composer pills, which return `{ update, remove }` handles. Preserve any remover the plugin calls before teardown; Paseo removes outstanding diff --git a/public-docs/plugins/reference.md b/public-docs/plugins/reference.md index 5e7e7a030f..4d84edb578 100644 --- a/public-docs/plugins/reference.md +++ b/public-docs/plugins/reference.md @@ -14,7 +14,7 @@ Migrating an existing plugin? Follow the standalone [runtime-entry migration gui Local plugins are directory sources installed into one Paseo daemon. A plugin can contribute: -- React Native surfaces and sidebar items to Paseo clients; +- React Native screens and sidebar header and footer items to Paseo clients; - workspace and agent panels opened as workspace tabs; - global, workspace, and agent actions in the Command Center; - slash commands in the message composer; @@ -98,7 +98,7 @@ and cannot show this new diagnostic. At least one entry is required; both accept `.ts` or `.tsx`. A directory that still has only the old `index.ts` fails to load and points at the [migration guide](/docs/plugins/migration). -Plugin, surface, sidebar-item, workspace-panel, Command Center item, attachment-source, and +Plugin, screen, sidebar-item, workspace-panel, Command Center item, attachment-source, and slash-command IDs start with a lowercase letter and contain lowercase letters, numbers, or hyphens. The generated `package.json` installs `@getpaseo/plugin` and the other host modules as development @@ -263,7 +263,7 @@ for remote workspaces; `localhost` URLs refer to that desktop. | Target host/workspace is unknown or its workspace list has not loaded | Throws `Workspace is unavailable on the requested host.` before creating a tab | Use the [settings API](#settings-screens) for typed host-scoped persistence across clients. -Use `openSettings`, `openSurface`, and `openPanel` for your own registered contributions. +Use `openSettings`, `openScreen`, and `openPanel` for your own registered contributions. ### Server runtime @@ -331,9 +331,9 @@ export default function contribute(server: PluginServerContext) { } ``` -`discover()` supplies configured inputs; return `[]` when no account is configured. The daemon also passes each live agent's `session.usage_reference` input to `identify()`. That method returns a stable account key and optional display label without fetching usage, or `null` when there are no credentials. The daemon combines the source ID and key as `:`. The key must be 1–128 characters from `[A-Za-z0-9._-]`, remain stable across token rotation and input routes, and identify the account or organization whose quota is metered. Never use a credential or raw email as the key; use `hashAccountKey(value)` when the only stable identity is sensitive. +`discover()` is required and supplies configured inputs; return `[]` when no account is configured. `identify(input)` returns a stable account key and optional display label without fetching usage, or `null` when there are no credentials. The daemon combines the source ID and key as `:`. The key must be 1–128 characters from `[A-Za-z0-9._-]`, remain stable across token rotation, and identify the account or organization whose quota is metered. Never use a credential or raw email as the key; use `hashAccountKey(value)` when the only stable identity is sensitive. -`usage.list_reports` discovers reports when called without IDs, or reads only the requested known IDs. It caches each report for five minutes and `forceRefresh` refreshes only the returned IDs. Each entry carries `id`, `account.label`, and `fetchedAt`; `fetch()` returns a `UsageReport` with `status` (`available`, `unavailable`, or `error`), optional `planLabel`, and generic `windows`, `balances`, and `details`. Set `headline: true` on a window to show it first. The icon is a path to a self-contained SVG under the plugin directory and follows the provider icon restrictions above. +`usage.list_reports` discovers reports when called without IDs, or reads only the requested known IDs. It caches each report for five minutes and `forceRefresh` refreshes only the returned IDs. Each entry carries `id`, `account.label`, and `fetchedAt`; `fetch()` returns a `UsageReport` with `status` (`available`, `unavailable`, or `error`), optional `planLabel`, and generic `windows`, `balances`, and `details`. The icon is a path to a self-contained SVG under the plugin directory and follows the provider icon restrictions above. ## Entry point and cleanup @@ -346,7 +346,7 @@ import type { PluginClientContext } from "@getpaseo/plugin/client"; import { Main } from "./client/main"; export default function contribute(client: PluginClientContext) { - client.addSurface("main", Main); + client.addScreen({ id: "main", title: "My plugin", Component: Main }); return () => {}; } ``` @@ -676,18 +676,19 @@ saved; environment overrides are not persisted with it. Read logger output with `paseo plugin logs lifecycle-logger` or the host's `daemon.log`. -## Surfaces and sidebar items +## Screens and sidebar items -Register a component, then point a sidebar item at its surface ID: +A screen is a full page of plugin UI. A sidebar item is a component Paseo renders in the sidebar +header or footer; it decides what a press does. Register the screen, then an item that opens it: `client/main.tsx`: ```tsx -import type { PluginSurfaceProps } from "@getpaseo/plugin/client"; +import type { PluginScreenProps } from "@getpaseo/plugin/client"; import { useMemo } from "react"; import { Text, View } from "react-native"; -export function Main({ theme, host, layout }: PluginSurfaceProps) { +export function Main({ theme, host, layout }: PluginScreenProps) { const styles = useMemo( () => ({ screen: { @@ -711,32 +712,180 @@ export function Main({ theme, host, layout }: PluginSurfaceProps) { `index.client.tsx`: -```ts -import type { PluginClientContext } from "@getpaseo/plugin/client"; +```tsx +import type { PluginClientContext, PluginSidebarItemProps } from "@getpaseo/plugin/client"; +import { SidebarRow } from "@getpaseo/plugin/client/ui"; import { Main } from "./client/main"; +function MainItem({ currentScreen, openScreen }: PluginSidebarItemProps) { + return ( + openScreen({ screenId: "main" })} + /> + ); +} + export default function contribute(client: PluginClientContext) { - client.addSurface("main", Main); - client.addSidebarItem({ - id: "main", - title: "My plugin", - icon: "Blocks", - surface: "main", - }); + client.addScreen({ id: "main", title: "My plugin", Component: Main }); + client.addSidebarHeaderItem({ id: "main", title: "My plugin", Component: MainItem }); return () => {}; } ``` -`PluginSurfaceProps` contains: +`PluginScreenProps` contains: | Field | Meaning | | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `theme` | Typed `PluginTheme` color tokens for the active Paseo theme. | | `host` | Selected host `id` and display `label`. | | `layout` | `compact` and the `ios`, `android`, or `web` platform. | +| `params` | The params the screen was opened with, as string keys and values. `{}` when none. | | `navigation` | Optional client navigation. `openAgent({ agentId, serverId? })` and `openWorkspace({ workspaceId, serverId? })` open targets on `serverId`, or on the selected host when omitted. `openBrowser({ url, workspaceId, serverId? })` is available only on Electron; see [links and browsers](#external-links-and-workspace-browsers). | -Paseo owns the route, header, close action, host picker, error boundary, and query client. The plugin owns the surface body. +Paseo owns the route, header, close action, host picker, error boundary, and query client. The plugin owns the screen body. + +`addScreen({ id, title, Component })` registers a screen. `title` is the screen header's title: a +string, or a function that takes the screen's params and returns one, so a bot screen can show the +bot's name. The function runs again when the params change. Registration throws on an empty title +or one that is neither a string nor a function. A title function that throws or returns an empty +string shows the screen ID instead; the screen still renders. + +`openScreen({ screenId, params })` opens a screen with params, such as the bot a bot screen shows. +Params live in the screen's URL query, so a reload, back and forward, and a link to the screen keep +them. Any string key works; values must be strings, and anything else throws. Opening the same +screen with other params shows the new params. + +### Sidebar items + +`addSidebarHeaderItem` adds a row to the list at the top of the sidebar. `addSidebarFooterItem` +adds a row to the footer, between **Add project** and the footer's icon row. The icon row is fixed; +plugins can't add to it. Both take `{ id, title, Component }`. `title` labels the item in +Settings > Sidebar, where users reorder and hide items, and is the default label and accessibility +label of `SidebarRow`. Settings shows a generic plugin icon for these items; a row from the +deprecated `addSidebarItem` keeps its registered icon. + +With the plugin on several hosts, an item renders from the host of the screen the app shows. Off a +host's screens, it uses the host last picked in one of the plugin's screens or last opened from one +of its items, else the first host. + +`Component` receives `PluginSidebarItemProps`: + +| Field | Meaning | +| ------------------------- | --------------------------------------------------------------------------------------------- | +| `theme`, `host`, `layout` | As in `PluginScreenProps`. | +| `currentScreen` | `{ screenId, params }` of this plugin's screen open on the item's host, or `null`. | +| `openScreen(input)` | Opens one of this plugin's screens: `{ screenId, params? }`. Unknown screen IDs throw. | +| `openPopover(Content)` | Opens `Content` anchored to the pressed row on wide layouts and as a bottom sheet on compact. | + +`Content` receives `theme`, `host`, `layout`, `close()`, and `openScreen(input)`. Opening a screen +closes the popover. + +Render the item with the sidebar kit from `@getpaseo/plugin/client/ui`: + +| Component | Props and behavior | +| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `SidebarRow` | Required `onPress`; optional `icon` (Lucide name or `{ size, color }` component), `label`, `active`, `trailing`, `id`. A full-width row. `trailing` renders beside the row's pressable, so a button in it presses on its own; a press elsewhere in it presses the row. `id` tells rows of one item apart in test IDs. | +| `SidebarSeparator` | No props. The sidebar's separator line, between groups of rows. | + +A popover anchors to the row that was pressed, whether the press landed on the row or on its +`trailing` content, so a per-row "More" button opens its popover next to its own row. + +```tsx +import type { PluginSidebarItemProps, PluginPopoverProps } from "@getpaseo/plugin/client"; +import { SidebarRow } from "@getpaseo/plugin/client/ui"; +import { Pressable, Text } from "react-native"; + +function SyncDetails({ theme, close, openScreen }: PluginPopoverProps) { + return ( + openScreen({ screenId: "sync" })}> + Open sync history + + ); +} + +function SyncItem({ openPopover }: PluginSidebarItemProps) { + return openPopover(SyncDetails)} />; +} + +client.addSidebarFooterItem({ id: "sync", title: "Sync", Component: SyncItem }); +``` + +An item that throws renders nothing; the rest of the sidebar keeps working. + +#### Several rows in one item + +An item's `Component` may return a fragment, an array, or `null`, so one item can render a list +that changes at runtime. Settings lists the item once, and it moves and hides as one block. This +item shows a row per bot and opens the bot's screen with its ID as a param: + +```tsx +import type { + PluginClientContext, + PluginScreenProps, + PluginSidebarItemProps, +} from "@getpaseo/plugin/client"; +import { SidebarRow, SidebarSeparator } from "@getpaseo/plugin/client/ui"; +import { Text } from "react-native"; +import { botName, useBots } from "./client/bots"; + +function BotScreen({ theme, params }: PluginScreenProps) { + return Bot {params.botId}; +} + +function BotsItem({ currentScreen, openScreen }: PluginSidebarItemProps) { + const bots = useBots(); + const openBotId = currentScreen?.screenId === "bot" ? currentScreen.params.botId : null; + return ( + <> + + {bots.map((bot) => ( + openScreen({ screenId: "bot", params: { botId: bot.id } })} + /> + ))} + + ); +} + +export default function contribute(client: PluginClientContext) { + client.addScreen({ + id: "bot", + title: (params) => botName(params.botId) ?? "Bot", + Component: BotScreen, + }); + client.addSidebarHeaderItem({ id: "bots", title: "Bots", Component: BotsItem }); + return () => {}; +} +``` + +#### Add and remove items at runtime + +`addSidebarHeaderItem` and `addSidebarFooterItem` work after the entry returns. Each returns a +remover; the item appears and disappears without a reload: + +```tsx +let removeAlerts: (() => void) | null = null; + +function showAlerts(client: PluginClientContext) { + removeAlerts ??= client.addSidebarFooterItem({ + id: "alerts", + title: "Alerts", + Component: AlertsItem, + }); +} + +function hideAlerts() { + removeAlerts?.(); + removeAlerts = null; +} +``` ## Host UI @@ -1399,12 +1548,12 @@ Every callback receives: | `context` | All | Matching discriminator. | | `paseo` | All | Selected host's existing `PaseoApi`. | | `rpc(contract, input)` | All | Typed call to this installation's daemon-side plugin handler. | -| `openSurface(id)` | All | Opens one of this plugin's registered global surfaces. | +| `openScreen(input)` | All | Opens one of this plugin's registered screens: `{ screenId, params? }`. | | `workspace` | Workspace and agent | Synchronous workspace snapshot. | | `agent` | Agent | Synchronous matching agent snapshot. | | `openPanel(id, options?)` | Workspace and agent | Opens a registered panel in the callback's current context. Pass `{ location: "explorer" }` to target Explorer. | -An agent callback may open either an agent panel or a workspace panel. A workspace callback may open only a workspace panel. Unknown surface and panel IDs fail visibly. Use `paseo` for normal workspace, agent, provider, and daemon-config operations. Use `rpc` for plugin-specific filesystem, credential, vendor, or daemon-local work. +An agent callback may open either an agent panel or a workspace panel. A workspace callback may open only a workspace panel. Unknown screen and panel IDs fail visibly. Use `paseo` for normal workspace, agent, provider, and daemon-config operations. Use `rpc` for plugin-specific filesystem, credential, vendor, or daemon-local work. ## Slash commands @@ -1609,6 +1758,27 @@ your subscriptions, timers, and other resources. Use `usePaseo()` for ordinary Paseo operations from a surface. It borrows the selected host's existing connection; do not create another client. +`usePaseo()` is your plugin's one Paseo client, the same `client.paseo` your setup receives, in +every surface. Release what you subscribe to in your cleanup; when the plugin is disabled, reloaded, +or removed, or its host goes away, Paseo disposes the client and ends every subscription still open. + +```tsx +import { usePaseo } from "@getpaseo/plugin/client"; +import { useEffect, useState } from "react"; +import { Text } from "react-native"; + +function ProjectChanges() { + const paseo = usePaseo(); + const [changes, setChanges] = useState(0); + useEffect(() => { + const observation = paseo.observeEvents(["project.update"]); + observation.subscribe({ snapshot() {}, update: () => setChanges((count) => count + 1) }); + return () => void observation.release(); + }, [paseo]); + return {changes} project changes; +} +``` + ```tsx import { type PluginSurfaceProps, usePaseo } from "@getpaseo/plugin/client"; import { Pressable, Text } from "react-native"; @@ -1768,7 +1938,7 @@ import type { PluginClientContext } from "@getpaseo/plugin/client"; import { GreetingButton } from "./client/greeting"; export default function contribute(client: PluginClientContext) { - client.addSurface("main", GreetingButton); + client.addScreen({ id: "main", title: "Greeting", Component: GreetingButton }); return () => {}; } ``` @@ -2080,16 +2250,16 @@ The switch is the root `pluginsEnabled` field in `config.json`. After changing i Use `paseo plugin ls` to read the current status and error. -| Symptom | Check | -| --------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | -| `This plugin was made for an older version of Paseo` | The directory has only an `index.ts` entry. Follow the [migration guide](/docs/plugins/migration). | -| `Plugin entry points are missing` | Neither `index.client.tsx` nor `index.server.ts` exists with that exact name. | -| `server-only module cannot be imported into the plugin client bundle` | Client code imports `server/`. Move the work behind an RPC and import its contract from `shared/`. | -| `client-only module cannot be imported into the plugin server bundle` | Server code imports `client/`. Register that contribution from `index.client.tsx` instead. | -| `Node module cannot be imported into the plugin client bundle` | Client code imports `node:*`. Move the operation to `server/` and call it through an RPC. | -| Sidebar item is missing | The plugin is `running`, the item references an existing surface, the icon name is valid, and the client is on the installation's host. | -| Client module is unavailable | Import only the host-provided client modules listed above. | -| RPC rejects | Check both Zod schemas and the daemon-side handler error. | -| Edited code does not appear | Run `npm run typecheck`, then `paseo plugin reload `. | -| Reload fails | Read `paseo plugin ls` and `paseo plugin logs `, fix the source error, then reload; Paseo does not restore the previous bundle. | -| Plugin exits unexpectedly | Read `paseo plugin logs ` for retained initialization, cleanup, stderr, and final crash output. | +| Symptom | Check | +| --------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | +| `This plugin was made for an older version of Paseo` | The directory has only an `index.ts` entry. Follow the [migration guide](/docs/plugins/migration). | +| `Plugin entry points are missing` | Neither `index.client.tsx` nor `index.server.ts` exists with that exact name. | +| `server-only module cannot be imported into the plugin client bundle` | Client code imports `server/`. Move the work behind an RPC and import its contract from `shared/`. | +| `client-only module cannot be imported into the plugin server bundle` | Server code imports `client/`. Register that contribution from `index.client.tsx` instead. | +| `Node module cannot be imported into the plugin client bundle` | Client code imports `node:*`. Move the operation to `server/` and call it through an RPC. | +| Sidebar item is missing | The plugin is `running`, the item is not hidden in Settings > Sidebar, its component does not throw, and the client is on the installation's host. | +| Client module is unavailable | Import only the host-provided client modules listed above. | +| RPC rejects | Check both Zod schemas and the daemon-side handler error. | +| Edited code does not appear | Run `npm run typecheck`, then `paseo plugin reload `. | +| Reload fails | Read `paseo plugin ls` and `paseo plugin logs `, fix the source error, then reload; Paseo does not restore the previous bundle. | +| Plugin exits unexpectedly | Read `paseo plugin logs ` for retained initialization, cleanup, stderr, and final crash output. | diff --git a/skills/paseo-plugin/SKILL.md b/skills/paseo-plugin/SKILL.md index f9a3086180..6fc8c90f87 100644 --- a/skills/paseo-plugin/SKILL.md +++ b/skills/paseo-plugin/SKILL.md @@ -1,6 +1,6 @@ --- name: paseo-plugin -description: Build and manage trusted local Paseo plugins. Use when the user asks to create, edit, install, reload, enable, disable, remove, or troubleshoot a Paseo plugin; add lifecycle hooks; transform agent configuration, environment, MCP servers, or workspace creation; automate permissions or turn follow-ups; add a native surface, sidebar item, or workspace panel; add Command Center items or slash commands; add composer pills or attachment sources; transform, render, or append agent timeline items; contribute a theme; use Paseo from plugin code; or add plugin RPCs. +description: Build and manage trusted local Paseo plugins. Use when the user asks to create, edit, install, reload, enable, disable, remove, or troubleshoot a Paseo plugin; add lifecycle hooks; transform agent configuration, environment, MCP servers, or workspace creation; automate permissions or turn follow-ups; add a screen, sidebar header or footer item, or workspace panel; add Command Center items or slash commands; add composer pills or attachment sources; transform, render, or append agent timeline items; contribute a theme; use Paseo from plugin code; or add plugin RPCs. --- # Paseo plugins @@ -27,21 +27,21 @@ public docs; this skill indexes the references and examples. Pick the contribution that matches the request. Each row names the registration, when it fits, and where the full contract lives. Most plugins combine several: a slash command that calls an RPC, which appends a timeline row, which a renderer draws. -| Contribution | Registration | Use it when | Reference | -| ------------------------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | -| Sidebar surface | `addSurface` + `addSidebarItem` | A full screen of plugin UI reachable from the sidebar | reference.md → Surfaces and sidebar items; `plugin-examples/local-plugin` | -| Workspace panel | `addWorkspacePanel` | UI that lives as a tab beside agents, terminals, files, and diffs; `locations: ["explorer"]` for the Explorer | reference.md → Workspace panels | -| Command Center item | `addCommandCenterItem` | A global, workspace, or agent action reachable from ⌘K | reference.md → Command Center items | -| Client slash command | `addSlashCommand` | A `/command args` in the composer that runs plugin code instead of prompting the agent | reference.md → Client slash commands | -| Composer pill | `addComposerPill` | A per-agent button in the composer track bar next to Tasks and Subagents | reference.md → Composer pills | -| Timeline transformer | `addTimelineTransformer` + `addTimelineRenderer` | Replace, explode, or hide a built-in timeline item, including while it streams | reference.md → Timeline items; `plugin-examples/timeline-items`, `plugin-examples/inline-thinking` | -| Timeline row | `paseo.agents.ref(id).timeline.append(...)` | Push a plugin-owned row into an agent timeline from a server handler and update it later | reference.md → Append a timeline row from the daemon | -| Attachment source | `client.addAttachmentSource` + `server.handle` | Let the user attach a searchable external resource, such as an issue, to a prompt | reference.md → Add a composer attachment source; `plugin-examples/linear` | -| Theme | `addTheme` | A light or dark palette under Settings → Appearance | reference.md → Contribute a theme; `plugin-examples/catppuccin` | -| Plugin RPC | `defineRpc` + `server.handle` + `useRpc` | Daemon-side work that is not a normal Paseo operation: vendor APIs, credentials, local files | reference.md → Add plugin-specific backend behavior | -| Lifecycle events | `server.on` | Observe agent/workspace lifecycle, inspect ended turns, and answer permission requests | [Lifecycle hooks](https://paseo.sh/docs/plugins/reference.md#lifecycle-hooks) | -| Creation and launch hooks | `server.before` | Change agent config, provider options, MCP servers, environment, or workspace isolation before the operation | [Before hooks](https://paseo.sh/docs/plugins/reference.md#before-hooks) | -| Paseo SDK | `usePaseo()` / handler `{ paseo }` | Normal Paseo operations: workspaces, agents, providers, config | reference.md → Use the Paseo SDK | +| Contribution | Registration | Use it when | Reference | +| ------------------------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | +| Screen and sidebar item | `addScreen` + `addSidebarHeaderItem`/`addSidebarFooterItem` | A full screen of plugin UI, or sidebar header or footer rows that open a screen or popover | reference.md → Screens and sidebar items | +| Workspace panel | `addWorkspacePanel` | UI that lives as a tab beside agents, terminals, files, and diffs; `locations: ["explorer"]` for the Explorer | reference.md → Workspace panels | +| Command Center item | `addCommandCenterItem` | A global, workspace, or agent action reachable from ⌘K | reference.md → Command Center items | +| Client slash command | `addSlashCommand` | A `/command args` in the composer that runs plugin code instead of prompting the agent | reference.md → Slash commands | +| Composer pill | `addComposerPill` | A per-agent button in the composer track bar next to Tasks and Subagents | reference.md → Composer pills | +| Timeline transformer | `addTimelineTransformer` + `addTimelineRenderer` | Replace, explode, or hide a built-in timeline item, including while it streams | reference.md → Timeline items; `plugin-examples/timeline-items`, `plugin-examples/inline-thinking` | +| Timeline row | `paseo.agents.ref(id).timeline.append(...)` | Push a plugin-owned row into an agent timeline from a server handler and update it later | reference.md → Append a timeline row from the daemon | +| Attachment source | `client.addAttachmentSource` + `server.handle` | Let the user attach a searchable external resource, such as an issue, to a prompt | reference.md → Add a composer attachment source; `plugin-examples/linear` | +| Theme | `addTheme` | A light or dark palette under Settings → Appearance | reference.md → Contribute a theme; `plugin-examples/catppuccin` | +| Plugin RPC | `defineRpc` + `server.handle` + `useRpc` | Daemon-side work that is not a normal Paseo operation: vendor APIs, credentials, local files | reference.md → Add plugin-specific backend behavior | +| Lifecycle events | `server.on` | Observe agent/workspace lifecycle, inspect ended turns, and answer permission requests | [Lifecycle hooks](https://paseo.sh/docs/plugins/reference.md#lifecycle-hooks) | +| Creation and launch hooks | `server.before` | Change agent config, provider options, MCP servers, environment, or workspace isolation before the operation | [Before hooks](https://paseo.sh/docs/plugins/reference.md#before-hooks) | +| Paseo SDK | `usePaseo()` / handler `{ paseo }` | Normal Paseo operations: workspaces, agents, providers, config | reference.md → Use the Paseo SDK | | Lifecycle task | Example | | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | @@ -189,18 +189,24 @@ export default function contribute(client: PluginClientContext) { Use `useWorkspace(id, selector)` and `useAgent(id, selector)`. Selectors are required and their results use shallow equality. Never select the whole snapshot or add an RPC to discover the active workspace or agent. Command callbacks receive the selected host's `paseo`, typed -`rpc(contract, input)`, `openSurface(id)`, and contextual `openPanel(id)` capabilities. +`rpc(contract, input)`, `openScreen({ screenId, params? })`, and contextual `openPanel(id)` +capabilities. -## Add a sidebar surface +## Add a screen and a sidebar item -Plugin surfaces use React Native primitives and work across desktop, browser, iOS, and Android. Register the surface before its sidebar item. Color text from `theme.colors` and pad from `layout.compact`. +Plugin screens use React Native primitives and work across desktop, browser, iOS, and Android. A sidebar item, in the header or the footer, is a component: render one or more `SidebarRow`s from `@getpaseo/plugin/client/ui`, with `SidebarSeparator` between groups, and call `openScreen` or `openPopover` from them. A screen's `title` is a string or a function of its `params`; its component receives the `params` it was opened with. Color text from `theme.colors` and pad from `layout.compact`. ```tsx -import type { PluginClientContext, PluginSurfaceProps } from "@getpaseo/plugin/client"; +import type { + PluginClientContext, + PluginScreenProps, + PluginSidebarItemProps, +} from "@getpaseo/plugin/client"; +import { SidebarRow } from "@getpaseo/plugin/client/ui"; import { useMemo, useState } from "react"; import { Pressable, Text, View } from "react-native"; -function Counter({ theme, layout }: PluginSurfaceProps) { +function Counter({ theme, layout }: PluginScreenProps) { const [count, setCount] = useState(0); const styles = useMemo( () => ({ @@ -231,19 +237,24 @@ function Counter({ theme, layout }: PluginSurfaceProps) { ); } +function CounterItem({ currentScreen, openScreen }: PluginSidebarItemProps) { + return ( + openScreen({ screenId: "main" })} + /> + ); +} + export default function contribute(client: PluginClientContext) { - client.addSurface("main", Counter); - client.addSidebarItem({ - id: "main", - title: "Counter", - icon: "ListPlus", - surface: "main", - }); + client.addScreen({ id: "main", title: "Counter", Component: Counter }); + client.addSidebarHeaderItem({ id: "main", title: "Counter", Component: CounterItem }); return () => {}; } ``` -Icons are Lucide icon names. `theme` is a typed `PluginTheme` on every surface and panel. Primary text uses `theme.colors.foreground`; labels use `theme.colors.foregroundMuted`; the root view uses `theme.colors.surface0`. `layout.compact` is true on mobile and narrow windows. Paseo owns the route, header, host picker, close action, error boundary, and per-installation query client. +Icons are Lucide icon names. `theme` is a typed `PluginTheme` on every screen and panel. Primary text uses `theme.colors.foreground`; labels use `theme.colors.foregroundMuted`; the root view uses `theme.colors.surface0`. `layout.compact` is true on mobile and narrow windows. Paseo owns the route, header, host picker, close action, error boundary, and per-installation query client. Before writing imports, classify each module as shared, client, or server. Follow the [SDK import boundaries](https://paseo.sh/docs/plugins/reference.md#runtime-modules), including @@ -278,7 +289,7 @@ Use the existing Paseo SDK for normal Paseo operations. Use plugin RPC only for ### Call Paseo from a surface -`usePaseo()` borrows the selected host's current connection. Never create another client inside a surface. +`usePaseo()` borrows the selected host's current connection. Never create another client inside a surface. It is the plugin's one client in every surface: release your subscriptions in your cleanup; plugin teardown ends the rest. ```tsx import { usePaseo } from "@getpaseo/plugin/client"; @@ -625,7 +636,7 @@ If the user asks to disable the global switch, set `pluginsEnabled` to `false`, Do not edit a local config when the target is a remote daemon. Perform the edit on the daemon machine, or ask the user to use **Settings → Plugins → Enable plugins**. `paseo reload --host ` reloads the remote daemon's own file but does not edit it. -When the same sidebar contribution exists on several connected hosts, Paseo shows it once with a host picker. The selected host owns the bundle, SDK calls, RPCs, and query cache. An offline selected host does not fall through to another host. Attachment sources stay scoped to the composer's host. +When the same screen or sidebar item exists on several connected hosts, Paseo shows it once. The screen header has a host picker; a sidebar item uses the host of the screen on display, else the host last picked in one of the plugin's screens. The selected host owns the bundle, SDK calls, RPCs, and query cache. An offline selected host does not fall through to another host. Attachment sources stay scoped to the composer's host. ## Typecheck and manage @@ -671,7 +682,7 @@ After a change: Common failures: -- Missing sidebar item: wrong host, plugin not `running`, invalid Lucide icon, or sidebar item points to a missing surface. +- Missing sidebar item: wrong host, plugin not `running`, the item is hidden in Settings > Sidebar, or its component throws (an invalid Lucide icon name throws). - Unavailable client module: client bundles can use only the host-provided modules listed above. - RPC rejection: input or output failed its Zod schema, or the handler threw. Inspect `paseo plugin logs ` for handler output. - Plugin exits or reload fails: inspect `paseo plugin ls` for status and `paseo plugin logs ` for initialization, cleanup, or crash output. From 8cccc1f6fa2cb7535240df2f42becc454eeb3676 Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Wed, 30 Sep 2026 14:09:56 +0200 Subject: [PATCH 015/100] fix(codex): answer each approval callback that shares one item (#5758) Codex sends several command approval requests for one item when it approves subcommands separately, telling them apart by approvalId. The permission id was derived from the item id alone, so the second request replaced the first and answering it left the first request unanswered. Key the permission on approvalId when Codex sends one. Closes #5615 Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> --- .../providers/codex-app-server-agent.test.ts | 70 +++++++++++++++++++ .../agent/providers/codex-app-server-agent.ts | 4 +- .../codex/test-utils/fake-app-server.ts | 10 +-- 3 files changed, 79 insertions(+), 5 deletions(-) diff --git a/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts b/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts index bb7d56d07a..ea52d982e8 100644 --- a/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts +++ b/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts @@ -10,6 +10,7 @@ import { fileURLToPath } from "node:url"; import type { AgentLaunchContext, + AgentPermissionRequest, AgentSession, AgentSessionConfig, AgentSlashCommand, @@ -1346,6 +1347,75 @@ describe("Codex app-server provider", () => { await session.close(); }); + test("answers each concurrent command approval callback that shares one item", async () => { + const appServer = createFakeCodexAppServer({ + initialize: () => ({}), + "collaborationMode/list": () => ({ data: [] }), + "skills/list": () => ({ data: [] }), + }); + const session = new CodexAppServerAgentSession( + createConfig({ cwd: "/workspace/project" }), + null, + createTestLogger(), + async () => appServer.child, + ); + const requested: AgentPermissionRequest[] = []; + session.subscribe((event) => { + if (event.type === "permission_requested") requested.push(event.request); + }); + + try { + await session.connect(); + + const firstPermission = waitForNextPermission(session); + appServer.requestCommandApproval({ + itemId: "shared-item", + approvalId: "callback-a", + threadId: "thread-1", + turnId: "turn-1", + command: "git add a.txt", + cwd: "/workspace/project", + reason: "subcommand a", + }); + await firstPermission; + const secondPermission = waitForNextPermission(session); + appServer.requestCommandApproval({ + itemId: "shared-item", + approvalId: "callback-b", + threadId: "thread-1", + turnId: "turn-1", + command: "git add b.txt", + cwd: "/workspace/project", + reason: "subcommand b", + }); + await secondPermission; + + expect(requested.map((request) => request.input?.command)).toEqual([ + "git add a.txt", + "git add b.txt", + ]); + expect(new Set(requested.map((request) => request.id)).size).toBe(2); + expect(session.getPendingPermissions()).toHaveLength(2); + + await session.respondToPermission(requested[0]!.id, { behavior: "allow" }); + await session.respondToPermission(requested[1]!.id, { + behavior: "deny", + message: "not b", + }); + + await expect(appServer.waitForCommandApprovalDecision("callback-a")).resolves.toEqual({ + decision: "accept", + }); + await expect(appServer.waitForCommandApprovalDecision("callback-b")).resolves.toEqual({ + decision: "decline", + }); + expect(session.getPendingPermissions()).toHaveLength(0); + appServer.assertNoErrors(); + } finally { + await session.close(); + } + }); + test("shows a successful shell command that produces no output", async () => { const appServer = createFakeCodexAppServer(); const session = new CodexAppServerAgentSession( diff --git a/packages/server/src/server/agent/providers/codex-app-server-agent.ts b/packages/server/src/server/agent/providers/codex-app-server-agent.ts index 119be789b9..b1dad5332c 100644 --- a/packages/server/src/server/agent/providers/codex-app-server-agent.ts +++ b/packages/server/src/server/agent/providers/codex-app-server-agent.ts @@ -6933,6 +6933,8 @@ export class CodexAppServerAgentSession implements AgentSession { const parsed = z .object({ itemId: z.string(), + // Set when several approval callbacks belong to one item; it names the callback. + approvalId: z.string().nullable().optional(), threadId: z.string(), turnId: z.string(), command: z.string().nullable().optional(), @@ -6946,7 +6948,7 @@ export class CodexAppServerAgentSession implements AgentSession { cwd: parsed.cwd ?? this.config.cwd ?? null, running: true, }); - const requestId = `permission-${parsed.itemId}`; + const requestId = `permission-${parsed.approvalId ?? parsed.itemId}`; const title = parsed.command ? `Run command: ${parsed.command}` : "Run command"; const request: AgentPermissionRequest = { id: requestId, diff --git a/packages/server/src/server/agent/providers/codex/test-utils/fake-app-server.ts b/packages/server/src/server/agent/providers/codex/test-utils/fake-app-server.ts index 31e231cdb1..713d744309 100644 --- a/packages/server/src/server/agent/providers/codex/test-utils/fake-app-server.ts +++ b/packages/server/src/server/agent/providers/codex/test-utils/fake-app-server.ts @@ -85,13 +85,15 @@ export interface FakeCodexAppServer { says(params: { threadId: string; itemId?: string; text: string; chunks?: string[] }): void; requestCommandApproval(params: { itemId: string; + approvalId?: string; threadId: string; turnId: string; command: string; cwd: string; reason: string; }): void; - waitForCommandApprovalDecision(itemId: string): Promise; + /** Keyed by `approvalId` when the request carried one, otherwise by `itemId`. */ + waitForCommandApprovalDecision(callbackId: string): Promise; requestFileChangeApproval(params: { itemId: string; threadId: string; @@ -518,7 +520,7 @@ export function createFakeCodexAppServer( requestCommandApproval(params) { const requestId = nextServerRequestId; nextServerRequestId += 1; - approvalRequestIds.set(params.itemId, requestId); + approvalRequestIds.set(params.approvalId ?? params.itemId, requestId); child.stdout.write( `${JSON.stringify({ jsonrpc: "2.0", @@ -528,8 +530,8 @@ export function createFakeCodexAppServer( })}\n`, ); }, - async waitForCommandApprovalDecision(itemId) { - return await this.waitForApprovalDecision(itemId); + async waitForCommandApprovalDecision(callbackId) { + return await this.waitForApprovalDecision(callbackId); }, requestFileChangeApproval(params) { const requestId = nextServerRequestId; From b0c9de0c90b6670186153344c5bf7b7b317b8aef Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Wed, 30 Sep 2026 13:46:35 +0100 Subject: [PATCH 016/100] =?UTF-8?q?feat(plugins):=20first-class=20plugin?= =?UTF-8?q?=20providers=20=E2=80=94=20status,=20launch=20resolution,=20con?= =?UTF-8?q?fig=20overrides=20(#5707)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(plugins): support provider status, launch resolution, and config overrides * fix(plugins): defer override warnings until provider startup settles --- docs/plugins.md | 40 ++- docs/providers.md | 2 +- .../src/server/acp-internal/connection.ts | 20 +- packages/plugin/src/server/acp.test.ts | 47 +++ packages/plugin/src/server/acp.ts | 1 + packages/plugin/src/server/provider.ts | 31 +- packages/protocol/src/provider-config.ts | 13 +- .../executable-resolution.ts | 50 ++- .../src/server/agent/plugin-provider.test.ts | 147 ++++++++ .../src/server/agent/plugin-provider.ts | 113 +++++-- .../server/agent/provider-launch-config.ts | 7 +- .../src/server/agent/provider-registry.ts | 107 ++++-- .../agent/provider-snapshot-manager.test.ts | 319 +++++++++++++++++- .../server/agent/provider-snapshot-manager.ts | 67 ++-- packages/server/src/server/bootstrap.ts | 1 + .../src/server/persisted-config.test.ts | 26 +- .../src/server/plugins/index.posix.test.ts | 85 ++++- packages/server/src/server/plugins/index.ts | 15 +- .../server/plugins/plugin-process-protocol.ts | 35 +- .../src/server/plugins/plugin-process.ts | 24 ++ packages/server/src/server/plugins/runtime.ts | 22 ++ 21 files changed, 1033 insertions(+), 139 deletions(-) diff --git a/docs/plugins.md b/docs/plugins.md index cf08b2a62a..f3c0572689 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -381,9 +381,38 @@ export default function contribute(server: PluginServerContext) { } ``` +Declare `command: ["agent", "serve"]` when your provider launches an executable. The daemon applies +`agents.providers..command` and `env`, resolves the executable against the effective PATH, and +removes parent-session and daemon-control environment variables. `connect({ launch })` receives +`{ command, args, env }`: an executable path, effective arguments, and the complete sanitized +environment. Spawn with those values; do not merge the plugin process's environment back in. +The launch travels as data through the same provider protocol for built-in and subprocess plugins. +Without a declared command, existing providers keep their connection behavior and receive no launch. + +Implement optional `status({ launch })` to check credentials, versions, or other prerequisites using +that effective launch. Return `{ available: true }` or +`{ available: false, diagnostic: "Run agent login" }`. The daemon checks executable availability +before calling status; it exposes your diagnostic through the normal provider diagnostic command. +A command-backed provider without status is available when its executable resolves. Providers with +neither command nor status retain connection-based availability. + +A same-ID config entry without `extends` overrides the plugin's `enabled`, `command`, `env`, label, +description, and model configuration through the normal provider registry. `enabled: false` disables +selection and discovery. An entry with `extends` defines the user's own provider, shadows the plugin, +and logs a warning. Overrides validate before plugins load; an override for an absent plugin stays +inactive and logs a warning naming the unmatched ID after built-in and configured plugin startup +settles (including disabled plugins), then on each installed registry generation. +Use IDs matching `/^[a-z][a-z0-9-]*$/` for configurable providers. Dots and underscores +remain valid for plugin registration but cannot be used as config provider IDs. + +Provider config changes rebuild the affected registry entry and invalidate its catalogue; the next +connection receives the new launch without restarting the daemon. Running sessions keep their +current launch until refreshed, as native providers do. + Implement optional `ProviderRegistration.getCatalogCacheKey(options)` to share equivalent catalogue probes. The callback runs in the plugin process before discovery and receives the actual global or -workspace target. Return a key covering effective configuration and execution environment, or +workspace target plus the resolved `launch` when a command is declared. Return a key covering +effective configuration and execution environment, or `undefined` for target-specific caching. Ignore `force` when choosing identity. Existing providers need no change. See [catalogue ownership](providers.md#provider-snapshot-refresh-contract). @@ -394,12 +423,15 @@ Provider settings are toggle/select data that Paseo renders in the composer. Kee the opaque `providerOptions` config object. Agent refresh closes the current provider session and opens it again with current configuration and -persistence. Providers re-read credentials, environment, global configuration, and MCP servers on -`session.open`; there is no provider reload input. +persistence. Re-read credentials and provider-owned configuration on `session.open`; consume the +daemon launch from `connect` and the per-session env and MCP servers from `session.open`. There is +no provider reload input. For an ACP command, register `runAcpProvider({ id, label, command })` from `@getpaseo/plugin/server/acp`. Its transformer hooks cover narrow vendor differences; do not translate the -whole provider event stream. The direct and ACP examples live in `plugin-examples/provider-direct` +whole provider event stream. The shim uses the resolved launch for every probe and session, overlaying +only the supplied per-session env for sessions. The direct and ACP examples live in +`plugin-examples/provider-direct` and `plugin-examples/provider-acp-transformer`. Provider-emitted plugin timeline items use the same renderer registration as transformed and diff --git a/docs/providers.md b/docs/providers.md index fa608ba7f0..87cdf527d2 100644 --- a/docs/providers.md +++ b/docs/providers.md @@ -295,7 +295,7 @@ export class CopilotACPAgentClient extends ACPAgentClient { ### 2. Add to the provider manifest -In `packages/server/src/server/agent/provider-manifest.ts`, add mode definitions with UI metadata (icons, color tiers) and a provider definition entry. +In `packages/protocol/src/provider-manifest.ts`, add mode definitions with UI metadata (icons, color tiers) and a provider definition entry. First, define the modes with visual metadata: diff --git a/packages/plugin/src/server/acp-internal/connection.ts b/packages/plugin/src/server/acp-internal/connection.ts index 5589cab825..a71f2ca696 100644 --- a/packages/plugin/src/server/acp-internal/connection.ts +++ b/packages/plugin/src/server/acp-internal/connection.ts @@ -40,6 +40,7 @@ import { type ProviderConnection, type ProviderEvent, type ProviderInput, + type ProviderLaunch, type ProviderPermissionResponse, type ProviderPersistence, type ProviderSessionConfig, @@ -69,6 +70,7 @@ export async function createAcpProviderConnection( } const probe = await AcpRuntime.start({ options, + launch: request.launch, boundarySessionId: "capability-probe", env: {}, emit: () => undefined, @@ -97,6 +99,7 @@ export async function createAcpProviderConnection( }; const state: AcpConnectionState = { options, + launch: request.launch, capabilities, sessions, emit, @@ -141,6 +144,7 @@ export async function createAcpProviderConnection( } interface AcpConnectionState { + launch?: ProviderLaunch; options: RunAcpProviderOptions; capabilities: readonly ProviderCapability[]; sessions: Map; @@ -245,6 +249,7 @@ async function discover( ): Promise { const runtime = await AcpRuntime.start({ options: state.options, + launch: state.launch, boundarySessionId: "catalog", env: {}, emit: state.emit, @@ -269,6 +274,7 @@ async function listSessions( ): Promise { const runtime = await AcpRuntime.start({ options: state.options, + launch: state.launch, boundarySessionId: "sessions", env: {}, emit: state.emit, @@ -299,6 +305,7 @@ async function openSession( throw new Error(`Session already exists: ${input.sessionId}`); const runtime = await AcpRuntime.start({ options: state.options, + launch: state.launch, boundarySessionId: input.sessionId, env: input.config.env, emit: state.emit, @@ -336,6 +343,7 @@ function requireSession(state: AcpConnectionState, sessionId: string): AcpBounda } interface StartRuntimeOptions { + launch?: ProviderLaunch; options: RunAcpProviderOptions; boundarySessionId: string; env: Readonly>; @@ -385,9 +393,15 @@ class AcpRuntime { let stream: Stream; let closeConnector = async () => {}; if (options.options.command) { - const [executable, ...args] = options.options.command; - child = spawn(executable, args, { - env: { ...process.env, ...options.env }, + // COMPAT(pluginProviderLaunch): added in v0.10.0, remove after 2027-03-29 once plugin host floor >= v0.10.0. + // Standalone callers and older hosts do not send a daemon-resolved launch. + const launch = options.launch ?? { + command: options.options.command[0], + args: options.options.command.slice(1), + env: process.env, + }; + child = spawn(launch.command, launch.args, { + env: { ...launch.env, ...options.env }, stdio: ["pipe", "pipe", "pipe"], }); spawnFailure = new Promise((_resolve, reject) => child!.once("error", reject)); diff --git a/packages/plugin/src/server/acp.test.ts b/packages/plugin/src/server/acp.test.ts index be52710dba..a62b2f2c48 100644 --- a/packages/plugin/src/server/acp.test.ts +++ b/packages/plugin/src/server/acp.test.ts @@ -225,6 +225,53 @@ lines.on("line", (line) => { });`; describe("runAcpProvider", () => { + it("spawns the daemon-resolved launch for probes, catalogues, listing, and sessions", async () => { + const executable = await fakeAcp(` +if (process.env.LAUNCH_TOKEN !== "resolved" || process.env.CLAUDECODE || process.env.PASEO_NODE_ENV) process.exit(1); +const readline = require("node:readline"); +const lines = readline.createInterface({ input: process.stdin }); +const send = (message) => process.stdout.write(JSON.stringify(message) + "\\n"); +lines.on("line", (line) => { + const message = JSON.parse(line); + let result; + if (message.method === "initialize") result = { protocolVersion: message.params.protocolVersion, agentCapabilities: { sessionCapabilities: { list: {} } } }; + else if (message.method === "session/new") result = { sessionId: "native", configOptions: [{ id: "model", name: "Model", category: "model", type: "select", currentValue: "resolved", options: [{ value: "resolved", name: process.env.SESSION_TOKEN || "probe" }] }] }; + else if (message.method === "session/list") result = { sessions: [] }; + else return; + send({ jsonrpc: "2.0", id: message.id, result }); +});`); + const registration = runAcpProvider({ + id: "launch-acp", + label: "Launch ACP", + command: ["missing-default-command"], + }); + const connection = await registration.connect({ + versions: [1], + capabilities: ["prompt.message", "session.list"], + launch: { command: process.execPath, args: [executable], env: { LAUNCH_TOKEN: "resolved" } }, + }); + const events: ProviderEvent[] = []; + connection.onEvent((event) => events.push(event)); + try { + await connection.send({ type: "catalog", requestId: "catalog" }); + expect(await waitForEvent(events, (event) => event.type === "catalog")).toMatchObject({ + catalog: { models: [{ id: "resolved", label: "probe" }] }, + }); + await connection.send({ type: "sessions", requestId: "list" }); + expect(await waitForEvent(events, (event) => event.type === "sessions")).toMatchObject({ + sessions: [], + }); + const input = openInput(); + input.config.env = { SESSION_TOKEN: "session" }; + await connection.send(input); + expect(await waitForEvent(events, (event) => event.type === "session.config")).toMatchObject({ + config: { models: [{ id: "resolved", label: "session" }] }, + }); + } finally { + await connection.close(); + } + }); + it("rejects connection cleanly when the ACP executable does not exist", async () => { const registration = runAcpProvider({ id: "missing-acp", diff --git a/packages/plugin/src/server/acp.ts b/packages/plugin/src/server/acp.ts index f737034a44..533453f15e 100644 --- a/packages/plugin/src/server/acp.ts +++ b/packages/plugin/src/server/acp.ts @@ -115,6 +115,7 @@ export function runAcpProvider(options: RunAcpProviderOptions): ProviderRegistra label: options.label, description: options.description, icon: options.icon, + command: options.command, connect(request) { return createAcpProviderConnection(options, request); }, diff --git a/packages/plugin/src/server/provider.ts b/packages/plugin/src/server/provider.ts index c65a814753..772d0d3eac 100644 --- a/packages/plugin/src/server/provider.ts +++ b/packages/plugin/src/server/provider.ts @@ -28,6 +28,9 @@ export type ProviderCapability = (typeof PROVIDER_CAPABILITIES)[number]; export interface ProviderRegistration { /** Equal keys share discovery within this provider. Include effective configuration and execution environment. */ getCatalogCacheKey?(options: ProviderCatalogOptions): Promise; + /** Default executable and arguments; the daemon resolves overrides before status/connect. */ + command?: readonly [string, ...string[]]; + status?(request: ProviderStatusRequest): Promise; id: string; label: string; description?: string; @@ -37,10 +40,34 @@ export interface ProviderRegistration { } export type ProviderCatalogOptions = - | { scope: "global"; force?: boolean } - | { scope: "workspace"; cwd: string; force?: boolean }; + | { scope: "global"; force?: boolean; launch?: ProviderLaunch } + | { scope: "workspace"; cwd: string; force?: boolean; launch?: ProviderLaunch }; + +export const ProviderLaunchSchema = z + .object({ + /** Resolved executable path. */ + command: z.string().min(1), + args: z.array(z.string()), + /** Complete daemon-owned environment, including overrides and parent-session stripping. */ + env: z.record(z.string(), z.string()), + }) + .strip(); + +export type ProviderLaunch = z.infer; + +export interface ProviderStatusRequest { + launch?: ProviderLaunch; +} + +export const ProviderStatusSchema = z.discriminatedUnion("available", [ + z.object({ available: z.literal(true), diagnostic: z.string().optional() }).strip(), + z.object({ available: z.literal(false), diagnostic: z.string().min(1) }).strip(), +]); + +export type ProviderStatus = z.infer; export interface ProviderConnectRequest { + launch?: ProviderLaunch; versions: readonly number[]; capabilities: readonly string[]; } diff --git a/packages/protocol/src/provider-config.ts b/packages/protocol/src/provider-config.ts index ccf73f23fd..7687c813c6 100644 --- a/packages/protocol/src/provider-config.ts +++ b/packages/protocol/src/provider-config.ts @@ -82,15 +82,10 @@ export const ProviderOverridesSchema = z } const isBuiltinProvider = builtinProviderIdSet.has(providerId); - if (!isBuiltinProvider && !provider.extends) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - path: [providerId, "extends"], - message: `Custom provider "${providerId}" must declare extends.`, - }); - } - - if (!isBuiltinProvider && !provider.label) { + // Config loads before plugin registration. An entry without extends is an override; + // registry construction decides whether its provider exists. Configurable plugin IDs + // use this existing config alphabet; dots/underscores remain registration-only. + if (!isBuiltinProvider && provider.extends && !provider.label) { ctx.addIssue({ code: z.ZodIssueCode.custom, path: [providerId, "label"], diff --git a/packages/server/src/executable-resolution/executable-resolution.ts b/packages/server/src/executable-resolution/executable-resolution.ts index d7af13a3d0..75f230409f 100644 --- a/packages/server/src/executable-resolution/executable-resolution.ts +++ b/packages/server/src/executable-resolution/executable-resolution.ts @@ -1,12 +1,16 @@ import { createRequire } from "node:module"; import { existsSync } from "node:fs"; +import type { ProcessEnvRecord } from "../server/paseo-env.js"; import { execCommand } from "../utils/spawn.js"; import { isWindowsCommandScript } from "../utils/windows-command.js"; import { windowsExecutableResolution } from "./windows.js"; export { quoteWindowsArgument, quoteWindowsCommand } from "../utils/windows-command.js"; -type Which = (command: string, options: { all: true }) => Promise; +type Which = ( + command: string, + options: { all: true; path?: string; pathExt?: string }, +) => Promise; const require = createRequire(import.meta.url); const which = require("which") as Which; @@ -16,16 +20,20 @@ function hasPathSeparator(value: string): boolean { return value.includes("/") || value.includes("\\"); } -async function enumerateCandidates(name: string): Promise { +async function enumerateCandidates(name: string, env?: ProcessEnvRecord): Promise { if (process.platform !== "win32" && existsSync("/usr/bin/which")) { - return enumerateCandidatesViaSystemWhich(name); + return enumerateCandidatesViaSystemWhich(name, env); } - return enumerateCandidatesViaLibrary(name); + return enumerateCandidatesViaLibrary(name, env); } -async function enumerateCandidatesViaSystemWhich(name: string): Promise { +async function enumerateCandidatesViaSystemWhich( + name: string, + env?: ProcessEnvRecord, +): Promise { try { const { stdout } = await execCommand("/usr/bin/which", ["-a", name], { + baseEnv: env, timeout: 3000, killSignal: "SIGKILL", }); @@ -37,10 +45,13 @@ async function enumerateCandidatesViaSystemWhich(name: string): Promise { +async function enumerateCandidatesViaLibrary( + name: string, + env?: ProcessEnvRecord, +): Promise { let candidates: string[]; try { - candidates = await which(name, { all: true }); + candidates = await which(name, { all: true, path: env?.PATH, pathExt: env?.PATHEXT }); } catch (error) { // `which` throws ENOENT when the command is absent from PATH. if ((error as NodeJS.ErrnoException).code === "ENOENT") { @@ -61,10 +72,13 @@ async function enumerateCandidatesViaLibrary(name: string): Promise { export async function probeExecutable( executablePath: string, - timeoutMs = PROBE_TIMEOUT_MS, + options: number | ExecutableResolutionOptions = PROBE_TIMEOUT_MS, ): Promise { + const { probeTimeoutMs: timeoutMs = PROBE_TIMEOUT_MS, env } = + typeof options === "number" ? { probeTimeoutMs: options } : options; try { await execCommand(executablePath, ["--version"], { + baseEnv: env, timeout: timeoutMs, killSignal: "SIGKILL", maxBuffer: 64 * 1024, @@ -110,10 +124,17 @@ export function executableExists( return exists(executablePath) ? executablePath : null; } +export interface ExecutableResolutionOptions { + probeTimeoutMs?: number; + env?: ProcessEnvRecord; +} + export async function findExecutable( name: string, - probeTimeoutMs = PROBE_TIMEOUT_MS, + options: number | ExecutableResolutionOptions = PROBE_TIMEOUT_MS, ): Promise { + const { probeTimeoutMs = PROBE_TIMEOUT_MS, env } = + typeof options === "number" ? { probeTimeoutMs: options } : options; const trimmed = name.trim(); if (!trimmed) { return null; @@ -121,20 +142,21 @@ export async function findExecutable( if (process.platform === "win32") { return windowsExecutableResolution.find(trimmed, { - enumeratePathCandidates: enumerateCandidates, - probeExecutable, + enumeratePathCandidates: (command) => enumerateCandidates(command, env), + probeExecutable: (command, timeout) => + probeExecutable(command, { probeTimeoutMs: timeout, env }), exists: existsSync, probeTimeoutMs, }); } if (hasPathSeparator(trimmed)) { - return (await probeExecutable(trimmed, probeTimeoutMs)) ? trimmed : null; + return (await probeExecutable(trimmed, { probeTimeoutMs, env })) ? trimmed : null; } - const candidates = await enumerateCandidates(trimmed); + const candidates = await enumerateCandidates(trimmed, env); for (const candidate of candidates) { - if (await probeExecutable(candidate, probeTimeoutMs)) { + if (await probeExecutable(candidate, { probeTimeoutMs, env })) { return candidate; } } diff --git a/packages/server/src/server/agent/plugin-provider.test.ts b/packages/server/src/server/agent/plugin-provider.test.ts index c18f30a108..e1976e22a0 100644 --- a/packages/server/src/server/agent/plugin-provider.test.ts +++ b/packages/server/src/server/agent/plugin-provider.test.ts @@ -1,5 +1,7 @@ +import { relative } from "node:path"; import type { ProviderConnection, + ProviderLaunch, ProviderEvent, ProviderInput, ProviderRegistration, @@ -817,3 +819,148 @@ describe("pending provider responses", () => { }, ); }); + +describe("plugin provider launch and status", () => { + test("resolves the executable against the provider's PATH override", async () => { + const harness = createProviderHarness(); + const registration: ProviderRegistration = { ...harness.registration, command: ["node"] }; + const logger = createTestLogger(); + const registry = new PluginAgentClientRegistry(logger); + registry.replace([registration]); + try { + const definition = registry.definitions()[registration.id]!; + const client = definition.createClient(logger, { env: { PATH: "/paseo-nonexistent-bin" } }); + expect(await client.isAvailable()).toBe(false); + expect(await client.getDiagnostic!()).toEqual({ diagnostic: "node not found on PATH" }); + } finally { + await registry.shutdown(); + } + }); + + test("strips parent-session and daemon-control variables from session env overlays", async () => { + const harness = createProviderHarness(); + const registry = new PluginAgentClientRegistry(createTestLogger()); + registry.replace([harness.registration]); + try { + const client = registry.clients()[harness.registration.id]!; + await client.createSession( + { provider: harness.registration.id, cwd: "/workspace" }, + { + env: { + SESSION_TOKEN: "session", + PASEO_AGENT_ID: "agent", + CLAUDECODE: "parent", + PASEO_NODE_ENV: "development", + }, + }, + ); + const input = harness.inputs.find((value) => value.type === "session.open"); + expect(input).toMatchObject({ + config: { env: { SESSION_TOKEN: "session", PASEO_AGENT_ID: "agent" } }, + }); + if (input?.type !== "session.open") throw new Error("Expected session.open"); + expect(input.config.env).not.toHaveProperty("CLAUDECODE"); + expect(input.config.env).not.toHaveProperty("PASEO_NODE_ENV"); + } finally { + await registry.shutdown(); + } + }); + + test("resolves relative executable paths before sending launch data", async () => { + const harness = createProviderHarness(); + const launches: Array = []; + const registration: ProviderRegistration = { + ...harness.registration, + command: [relative(process.cwd(), process.execPath)], + async connect(request) { + launches.push(request.launch); + return harness.registration.connect(request); + }, + }; + const registry = new PluginAgentClientRegistry(createTestLogger()); + registry.replace([registration]); + try { + await registry.clients()[registration.id]!.fetchCatalog({ scope: "global" }); + expect(launches).toMatchObject([{ command: process.execPath }]); + } finally { + await registry.shutdown(); + } + }); + + test("a missing executable is unavailable with a diagnostic without connecting", async () => { + const harness = createProviderHarness(); + const registration: ProviderRegistration = { + ...harness.registration, + command: ["paseo-nonexistent-provider-executable"], + }; + const registry = new PluginAgentClientRegistry(createTestLogger()); + registry.replace([registration]); + try { + const client = registry.clients()[registration.id]!; + expect(await client.isAvailable()).toBe(false); + expect(await client.getDiagnostic!()).toEqual({ + diagnostic: "paseo-nonexistent-provider-executable not found on PATH", + }); + expect(harness.inputs).toEqual([]); + } finally { + await registry.shutdown(); + } + }); + + test.each([ + { mode: "default" as const }, + { mode: "append" as const, args: ["extra"] }, + { mode: "replace" as const, argv: [process.execPath, "replacement"] }, + ])( + "resolves $mode launch and supplies identical data to status, catalogue identity, and connect", + async (command) => { + const harness = createProviderHarness(); + const launches: Array = []; + const registration: ProviderRegistration = { + ...harness.registration, + command: [process.execPath, "default"], + async status(request) { + launches.push(request.launch); + return { available: true }; + }, + async getCatalogCacheKey(options) { + launches.push(options.launch); + return "shared"; + }, + async connect(request) { + launches.push(request.launch); + return harness.registration.connect(request); + }, + }; + const logger = createTestLogger(); + const registry = new PluginAgentClientRegistry(logger); + registry.replace([registration]); + try { + const client = registry.definitions()[registration.id]!.createClient(logger, { + command, + env: { PLUGIN_SETTING: "custom", CLAUDECODE: "parent" }, + }); + expect(await client.isAvailable()).toBe(true); + expect(await client.getCatalogCacheKey!({ scope: "global" })).toBe("shared"); + await client.fetchCatalog({ scope: "global" }); + let expectedArgs = ["default"]; + if (command.mode === "replace") expectedArgs = ["replacement"]; + if (command.mode === "append") expectedArgs = ["default", "extra"]; + expect(launches).toHaveLength(3); + for (const launch of launches) { + expect(launch).toMatchObject({ + command: process.execPath, + args: expectedArgs, + env: { PLUGIN_SETTING: "custom" }, + }); + expect(launch!.env).not.toHaveProperty("CLAUDECODE"); + expect(launch!.env).not.toHaveProperty("PASEO_NODE_ENV"); + } + expect(launches[0]).toEqual(launches[1]); + expect(launches[1]).toEqual(launches[2]); + } finally { + await registry.shutdown(); + } + }, + ); +}); diff --git a/packages/server/src/server/agent/plugin-provider.ts b/packages/server/src/server/agent/plugin-provider.ts index 1c26066fa8..af936a0f7f 100644 --- a/packages/server/src/server/agent/plugin-provider.ts +++ b/packages/server/src/server/agent/plugin-provider.ts @@ -1,4 +1,5 @@ import { randomUUID } from "node:crypto"; +import { resolve as resolvePath } from "node:path"; import type { Logger } from "pino"; import type { JsonValue, ProviderOptions } from "@getpaseo/protocol/agent-types"; import { z } from "zod"; @@ -7,6 +8,9 @@ import { PROVIDER_PROTOCOL_VERSION, ProviderEventSchema, ProviderInputSchema, + ProviderStatusSchema, + type ProviderStatus, + type ProviderLaunch, requireProviderCapabilities, type ProviderCapability, type ProviderConfigChanges, @@ -53,10 +57,12 @@ import type { SteerResult, } from "./agent-sdk-types.js"; import { - isDefaultAgentCreateConfigUnattended, - resolveDefaultAgentCreateConfig, -} from "./create-agent-mode.js"; -import type { ProviderDefinition } from "./provider-registry.js"; + resolveProviderLaunch, + checkProviderLaunchAvailable, + createProviderEnv, + type ProviderRuntimeSettings, +} from "./provider-launch-config.js"; +import type { RegisteredProviderDefinition } from "./provider-registry.js"; import { runProviderTurn } from "./providers/provider-runner.js"; import { StaleProviderSessionError } from "./stale-provider-session-error.js"; @@ -136,7 +142,46 @@ class ProviderRuntime { PendingOpenDescendantState >(); - constructor(private readonly registration: ProviderRegistration) {} + constructor( + private readonly registration: ProviderRegistration, + private readonly runtimeSettings?: ProviderRuntimeSettings, + ) {} + + private async resolveLaunch(): Promise { + const command = this.registration.command; + if (!command) return undefined; + const launch = await resolveProviderLaunch({ + commandConfig: this.runtimeSettings?.command, + defaultBinary: command[0], + }); + if (launch.source !== "override") launch.args.unshift(...command.slice(1)); + const env = createProviderEnv({ runtimeSettings: this.runtimeSettings }); + launch.env = env; + const availability = await checkProviderLaunchAvailable(launch); + if (!availability.resolvedPath) throw new Error(`${launch.command} not found on PATH`); + return { command: resolvePath(availability.resolvedPath), args: launch.args, env }; + } + + async status(): Promise { + try { + const launch = await this.resolveLaunch(); + if (this.registration.status) + return ProviderStatusSchema.parse(await this.registration.status({ launch })); + if (!this.registration.command) await this.getConnection(); + return { available: true }; + } catch (error) { + return { + available: false, + diagnostic: error instanceof Error ? error.message : String(error), + }; + } + } + + async catalogCacheKey(options: FetchCatalogOptions): Promise { + const callback = this.registration.getCatalogCacheKey; + if (!callback) return undefined; + return callback({ ...options, launch: await this.resolveLaunch() }); + } get negotiatedCapabilities(): readonly string[] { return this.connection?.capabilities ?? []; @@ -147,8 +192,7 @@ class ProviderRuntime { } async isAvailable(): Promise { - await this.getConnection(); - return true; + return (await this.status()).available; } async catalog( @@ -328,6 +372,7 @@ class ProviderRuntime { private async establishConnection(generation: number): Promise { const rawConnection = await this.registration.connect({ + launch: await this.resolveLaunch(), versions: [PROVIDER_PROTOCOL_VERSION], capabilities: PROVIDER_CAPABILITIES, }); @@ -765,7 +810,8 @@ function normalizeConnection(connection: ProviderConnection): ProviderConnection interface AdaptedPluginProvider { registration: ProviderRegistration; client: PluginAgentClient; - definition: ProviderDefinition; + definition: RegisteredProviderDefinition; + clients: Set; } /** Owns the complete plugin-provider adaptation behind the existing core provider boundary. */ @@ -786,7 +832,7 @@ export class PluginAgentClientRegistry { for (const [id, adapted] of this.providers) { if (incoming.get(id) === adapted.registration) continue; this.providers.delete(id); - void adapted.client.shutdown().catch((error) => { + void Promise.all([...adapted.clients].map((client) => client.shutdown())).catch((error) => { this.logger.warn({ err: error, provider: id }, "Failed to stop plugin provider"); }); } @@ -794,15 +840,20 @@ export class PluginAgentClientRegistry { for (const [id, registration] of incoming) { if (this.providers.has(id)) continue; const client = new PluginAgentClient(registration); - this.providers.set(id, { + const clients = new Set([client]); + const definition = createPluginProviderDefinition( registration, - client, - definition: createPluginProviderDefinition(registration, client), - }); + (_logger, runtimeSettings) => { + const configuredClient = new PluginAgentClient(registration, runtimeSettings); + clients.add(configuredClient); + return configuredClient; + }, + ); + this.providers.set(id, { registration, client, definition, clients }); } } - definitions(): Record { + definitions(): Record { return Object.fromEntries( [...this.providers].map(([id, provider]) => [id, provider.definition]), ); @@ -819,7 +870,9 @@ export class PluginAgentClientRegistry { async shutdown(): Promise { const providers = [...this.providers.values()]; this.providers.clear(); - await Promise.all(providers.map(({ client }) => client.shutdown())); + await Promise.all( + providers.flatMap(({ clients }) => [...clients].map((client) => client.shutdown())), + ); } } @@ -827,28 +880,18 @@ const PluginProviderOptionsSchema: z.ZodType = z.record(z.strin function createPluginProviderDefinition( registration: ProviderRegistration, - client: PluginAgentClient, -): ProviderDefinition { + createClient: RegisteredProviderDefinition["createClient"], +): RegisteredProviderDefinition { return { id: registration.id, - configuration: null, label: registration.label, description: registration.description ?? `Plugin provider ${registration.label}`, iconSvg: registration.icon, defaultModeId: null, modes: [], - enabled: true, - derivedFromProviderId: null, optionsSchema: PluginProviderOptionsSchema, supportsExactMcpPreapproval: true, - validateOptions: (options) => - options === undefined ? undefined : PluginProviderOptionsSchema.parse(options), - applyOptions: (config, options) => ({ ...config, providerOptions: options }), - applyToolPolicy: (config, toolPolicy) => ({ ...config, toolPolicy }), - createClient: () => client, - resolveCreateConfig: resolveDefaultAgentCreateConfig, - isCreateConfigUnattended: isDefaultAgentCreateConfigUnattended, - fetchCatalog: (options, _client, context) => client.fetchCatalog(options, context), + createClient, }; } @@ -865,10 +908,11 @@ class PluginAgentClient implements AgentClient { readonly getCatalogCacheKey?: AgentClient["getCatalogCacheKey"]; - constructor(registration: ProviderRegistration) { - this.getCatalogCacheKey = registration.getCatalogCacheKey?.bind(registration); + constructor(registration: ProviderRegistration, runtimeSettings?: ProviderRuntimeSettings) { this.provider = registration.id; - this.runtime = new ProviderRuntime(registration); + this.runtime = new ProviderRuntime(registration, runtimeSettings); + if (registration.getCatalogCacheKey) + this.getCatalogCacheKey = (options) => this.runtime.catalogCacheKey(options); this.runtime.onSessionOpened((session, opened) => this.acceptChild(session, opened)); } @@ -934,6 +978,11 @@ class PluginAgentClient implements AgentClient { return await this.runtime.isAvailable(); } + async getDiagnostic(): Promise<{ diagnostic: string }> { + const status = await this.runtime.status(); + return { diagnostic: status.diagnostic ?? "Provider is available" }; + } + async listImportableSessions( options: ListImportableSessionsOptions = {}, ): Promise { @@ -1566,7 +1615,7 @@ function mapSessionConfig( ): ProviderSessionConfig { return { cwd: config.cwd, - env: { ...launchContext?.env }, + env: createProviderEnv({ baseEnv: {}, overlays: [launchContext?.env] }), systemPrompt: combineSystemPrompts(config.systemPrompt, config.daemonAppendSystemPrompt), mcpServers: { ...config.mcpServers }, toolPolicy: config.toolPolicy diff --git a/packages/server/src/server/agent/provider-launch-config.ts b/packages/server/src/server/agent/provider-launch-config.ts index fddd83b626..29554fd2b2 100644 --- a/packages/server/src/server/agent/provider-launch-config.ts +++ b/packages/server/src/server/agent/provider-launch-config.ts @@ -43,6 +43,7 @@ export interface ResolvedProviderLaunch { command: string; args: string[]; source: ProviderLaunchSource; + env?: ProcessEnvRecord; } export interface ProviderLaunchAvailability { @@ -64,8 +65,8 @@ function normalizeLaunchDefault( return defaultBinary; } -async function resolveLaunchPath(command: string): Promise { - const found = await findExecutable(command); +async function resolveLaunchPath(command: string, env?: ProcessEnvRecord): Promise { + const found = await findExecutable(command, { env }); if (found) { return found; } @@ -120,7 +121,7 @@ export async function checkProviderLaunchAvailable( const resolvedPath = defaultBinary && launch.source !== "override" ? await resolveDefaultLaunchPath(defaultBinary) - : await resolveLaunchPath(launch.command); + : await resolveLaunchPath(launch.command, launch.env); return { available: resolvedPath !== null, resolvedPath, diff --git a/packages/server/src/server/agent/provider-registry.ts b/packages/server/src/server/agent/provider-registry.ts index 569e842125..004f16c6c4 100644 --- a/packages/server/src/server/agent/provider-registry.ts +++ b/packages/server/src/server/agent/provider-registry.ts @@ -105,7 +105,16 @@ export interface ProviderDefinition extends AgentProviderDefinition { ) => Promise; } +/** Provider metadata and an unconfigured factory; the registry owns override application. */ +export interface RegisteredProviderDefinition extends AgentProviderDefinition { + iconSvg?: string; + optionsSchema: z.ZodType; + supportsExactMcpPreapproval: boolean; + createClient: (logger: Logger, runtimeSettings?: ProviderRuntimeSettings) => AgentClient; +} + export interface BuildProviderRegistryOptions { + pluginProviders?: Record; runtimeSettings?: AgentProviderRuntimeSettingsMap; providerOverrides?: Record; workspaceGitService?: Pick; @@ -492,8 +501,11 @@ function wrapClientProvider( return { provider, - capabilities: inner.capabilities, - createSession: async (config, launchContext) => + get capabilities() { + return inner.capabilities; + }, + shutdown: inner.shutdown?.bind(inner), + createSession: async (config, launchContext, options) => wrapSessionProvider( provider, await inner.createSession( @@ -502,6 +514,7 @@ function wrapClientProvider( provider: inner.provider, }, launchContext, + options, ), ), resumeSession: async (handle, overrides, launchContext, options) => @@ -713,6 +726,31 @@ function createResolvedProviderClient( ); } +interface RegisteredProvider { + definition: AgentProviderDefinition; + createClient: ProviderClientFactory; + contract: ProviderContract; + runtimeSettings?: ProviderRuntimeSettings; + override?: ProviderOverride; +} + +function resolveRegisteredProvider(input: RegisteredProvider): ResolvedProvider { + const { definition, override, contract } = input; + const runtimeSettings = mergeRuntimeSettings(input.runtimeSettings, toRuntimeSettings(override)); + return { + definition: applyOverrideToDefinition(definition, override), + runtimeSettings, + profileModels: override?.models ?? [], + additionalModels: override?.additionalModels ?? [], + profileModelsAreAdditive: false, + enabled: override?.enabled ?? definition.enabledByDefault ?? true, + derivedFromProviderId: null, + providerParams: override?.params, + createBaseClient: (logger) => input.createClient(logger, runtimeSettings), + contract, + }; +} + function buildResolvedBuiltinProviders( providerOverrides: Record, runtimeSettings: AgentProviderRuntimeSettingsMap | undefined, @@ -731,30 +769,23 @@ function buildResolvedBuiltinProviders( for (const definition of definitions) { const override = providerOverrides[definition.id]; const factory = getProviderClientFactory(definition.id); - const mergedRuntimeSettings = mergeRuntimeSettings( - runtimeSettings?.[definition.id], - toRuntimeSettings(override), + resolvedProviders.set( + definition.id, + resolveRegisteredProvider({ + definition, + override, + runtimeSettings: runtimeSettings?.[definition.id], + createClient: (logger, settings) => + factory(logger, settings, { + workspaceGitService: options.workspaceGitService, + managedProcesses: options.managedProcesses, + ompRuntime: options.ompRuntime, + openCodeBridge: options.openCodeBridge, + providerParams: override?.params, + }), + contract: PROVIDER_CONTRACTS[definition.id] ?? UNSUPPORTED_PROVIDER_CONTRACT, + }), ); - - resolvedProviders.set(definition.id, { - definition: applyOverrideToDefinition(definition, override), - runtimeSettings: mergedRuntimeSettings, - profileModels: override?.models ?? [], - additionalModels: override?.additionalModels ?? [], - profileModelsAreAdditive: false, - enabled: override?.enabled ?? definition.enabledByDefault ?? true, - derivedFromProviderId: null, - providerParams: override?.params, - createBaseClient: (logger) => - factory(logger, mergedRuntimeSettings, { - workspaceGitService: options.workspaceGitService, - managedProcesses: options.managedProcesses, - ompRuntime: options.ompRuntime, - openCodeBridge: options.openCodeBridge, - providerParams: override?.params, - }), - contract: PROVIDER_CONTRACTS[definition.id] ?? UNSUPPORTED_PROVIDER_CONTRACT, - }); } return resolvedProviders; @@ -771,7 +802,8 @@ function addDerivedProviders( } if (!override.extends) { - throw new Error(`Custom provider '${providerId}' requires an extends value`); + // Plugin overrides can precede registration (including disabled or removed plugins). + continue; } if (override.extends === "acp") { @@ -889,6 +921,29 @@ export function buildProviderRegistry( }, options?.isDev === true, ); + for (const [provider, definition] of Object.entries(options?.pluginProviders ?? {})) { + if (resolvedProviders.has(provider)) { + throw new Error(`Plugin provider '${provider}' conflicts with a built-in provider`); + } + const override = providerOverrides[provider]; + if (override?.extends) { + logger.warn({ provider }, "Plugin provider shadowed by configured provider"); + continue; + } + resolvedProviders.set( + provider, + resolveRegisteredProvider({ + definition, + override, + runtimeSettings: runtimeSettings?.[provider], + createClient: definition.createClient, + contract: { + optionsSchema: definition.optionsSchema, + supportsExactMcpPreapproval: definition.supportsExactMcpPreapproval, + }, + }), + ); + } addDerivedProviders(resolvedProviders, providerOverrides, { managedProcesses: options?.managedProcesses, openCodeBridge: options?.openCodeBridge, diff --git a/packages/server/src/server/agent/provider-snapshot-manager.test.ts b/packages/server/src/server/agent/provider-snapshot-manager.test.ts index 3f81e5e9c7..b8c59d739d 100644 --- a/packages/server/src/server/agent/provider-snapshot-manager.test.ts +++ b/packages/server/src/server/agent/provider-snapshot-manager.test.ts @@ -2,7 +2,11 @@ import pino from "pino"; import { homedir } from "node:os"; import { resolve } from "node:path"; import { describe, expect, test, vi } from "vitest"; -import type { ProviderEvent, ProviderRegistration } from "@getpaseo/plugin/server/provider"; +import type { + ProviderEvent, + ProviderRegistration, + ProviderLaunch, +} from "@getpaseo/plugin/server/provider"; import { createTestLogger } from "../../test-utils/test-logger.js"; import type { @@ -3448,3 +3452,316 @@ test("binding a settled catalogue publishes once and rebinding an equal settled manager.destroy(); } }); + +describe("plugin provider overrides and launch", () => { + function provider() { + const launches: Array = []; + const registration: ProviderRegistration = { + id: "launch-plugin", + label: "Launch plugin", + command: [process.execPath, "default.js"], + async status({ launch }) { + if (launch?.env.PLUGIN_LOGIN !== "yes") { + return { available: false, diagnostic: "Sign in to the provider" }; + } + return { available: true }; + }, + async connect(request) { + launches.push(request.launch); + let listener: ((event: ProviderEvent) => void) | undefined; + return { + version: 1, + capabilities: ["session.persistence"], + async send(input) { + if (input.type !== "catalog") return; + listener?.({ + type: "catalog", + requestId: input.requestId, + catalog: { models: [{ id: "runtime", label: "Runtime" }], modes: [] }, + }); + }, + onEvent(next) { + listener = next; + return () => { + listener = undefined; + }; + }, + async close() {}, + }; + }, + }; + return { registration, launches }; + } + + test("reports plugin status diagnostics without connecting", async () => { + const { registration, launches } = provider(); + const manager = new ProviderSnapshotManager({ logger: createTestLogger() }); + try { + manager.replacePluginProviders([registration]); + expect(await manager.getProvider({ provider: registration.id, wait: true })).toMatchObject({ + status: "unavailable", + }); + expect((await manager.getProviderDiagnostic(registration.id)).diagnostic).toContain( + "Sign in to the provider", + ); + expect(launches).toEqual([]); + } finally { + await manager.shutdown(); + manager.destroy(); + } + }); + + test("applies plugin overrides and refreshes the resolved launch on config edits", async () => { + const { registration, launches } = provider(); + const manager = new ProviderSnapshotManager({ + logger: createTestLogger(), + providerOverrides: { + [registration.id]: { + enabled: false, + label: "Configured plugin", + models: [{ id: "configured", label: "Configured" }], + additionalModels: [{ id: "extra", label: "Extra" }], + command: [process.execPath, "override.js"], + env: { PLUGIN_LOGIN: "yes", CLAUDECODE: "parent" }, + }, + }, + }); + try { + manager.replacePluginProviders([registration]); + expect(await manager.getProvider({ provider: registration.id, wait: true })).toMatchObject({ + enabled: false, + status: "unavailable", + label: "Configured plugin", + }); + expect(launches).toEqual([]); + manager.applyMutableProviderConfig({ [registration.id]: { enabled: true } }); + expect(await manager.getProvider({ provider: registration.id, wait: true })).toMatchObject({ + status: "ready", + }); + expect( + (await manager.getProvider({ provider: registration.id })).models?.map((model) => model.id), + ).toEqual(["configured", "extra"]); + expect(launches[0]).toMatchObject({ + command: process.execPath, + args: ["override.js"], + env: { PLUGIN_LOGIN: "yes" }, + }); + expect(launches[0]!.env).not.toHaveProperty("CLAUDECODE"); + const previous = manager.getAgentManagerProviderState().clients[registration.id]; + manager.applyMutableProviderConfig({ + [registration.id]: { + enabled: true, + command: [process.execPath, "edited.js"], + env: { PLUGIN_LOGIN: "no" }, + }, + }); + expect(manager.getAgentManagerProviderState().clients[registration.id]).not.toBe(previous); + expect(await manager.getProvider({ provider: registration.id, wait: true })).toMatchObject({ + status: "unavailable", + }); + manager.applyMutableProviderConfig({ + [registration.id]: { + enabled: true, + command: [process.execPath, "edited.js"], + env: { PLUGIN_LOGIN: "yes" }, + }, + }); + expect(await manager.getProvider({ provider: registration.id, wait: true })).toMatchObject({ + status: "ready", + }); + expect(launches.at(-1)).toMatchObject({ args: ["edited.js"] }); + } finally { + await manager.shutdown(); + manager.destroy(); + } + }); + + test("does not warn for a plugin override during startup before provider registration settles", async () => { + const { registration } = provider(); + const warnings: string[] = []; + const logger = pino( + { level: "warn" }, + { + write(line: string) { + warnings.push(line); + }, + }, + ); + const manager = new ProviderSnapshotManager({ + logger, + providerOverrides: { + [registration.id]: { command: [process.execPath], env: { PLUGIN_LOGIN: "yes" } }, + }, + }); + try { + expect(warnings).toEqual([]); + manager.replacePluginProviders([]); + expect(warnings).toEqual([]); + manager.replacePluginProviders([registration]); + manager.settlePluginProviders(); + expect(warnings).toEqual([]); + expect(await manager.getProvider({ provider: registration.id, wait: true })).toMatchObject({ + status: "ready", + }); + } finally { + await manager.shutdown(); + manager.destroy(); + } + }); + + test("warns for unknown overrides when plugin startup settles without registered plugins", async () => { + const warnings: string[] = []; + const logger = pino( + { level: "warn" }, + { + write(line: string) { + warnings.push(line); + }, + }, + ); + const manager = new ProviderSnapshotManager({ + logger, + providerOverrides: { "typo-provider": { enabled: false } }, + }); + try { + expect(warnings).toEqual([]); + manager.settlePluginProviders(); + manager.settlePluginProviders(); + expect(warnings).toHaveLength(1); + expect(JSON.parse(warnings[0]!)).toMatchObject({ + provider: "typo-provider", + msg: "Provider override matches no registered provider", + }); + } finally { + await manager.shutdown(); + manager.destroy(); + } + }); + + test("warns once for unknown overrides after plugin startup settles without warning for known providers", async () => { + const { registration } = provider(); + const warnings: string[] = []; + const logger = pino( + { level: "warn" }, + { + write(line: string) { + warnings.push(line); + }, + }, + ); + const manager = new ProviderSnapshotManager({ + logger, + providerOverrides: { + "typo-provider": { enabled: false }, + [registration.id]: { enabled: false }, + codex: { enabled: false }, + }, + }); + try { + expect(warnings).toEqual([]); + manager.replacePluginProviders([registration]); + expect(warnings).toEqual([]); + manager.settlePluginProviders(); + manager.settlePluginProviders(); + const unknown = warnings.map((line) => JSON.parse(line) as { provider: string; msg: string }); + expect(unknown).toEqual([ + expect.objectContaining({ + provider: "typo-provider", + msg: "Provider override matches no registered provider", + }), + ]); + } finally { + await manager.shutdown(); + manager.destroy(); + } + }); + + test("a same-ID extends entry shadows the plugin without throwing", async () => { + const { registration, launches } = provider(); + const warnings: string[] = []; + const logger = pino( + { level: "warn" }, + { + write(line: string) { + warnings.push(line); + }, + }, + ); + const manager = new ProviderSnapshotManager({ + logger, + providerOverrides: { + [registration.id]: { + extends: "acp", + label: "Own provider", + command: [process.execPath], + enabled: true, + }, + }, + }); + try { + const previous = manager.getAgentManagerProviderState().clients[registration.id]; + manager.replacePluginProviders([registration]); + expect(manager.getAgentManagerProviderState().clients[registration.id]).toBe(previous); + expect(manager.getProviderLabel(registration.id)).toBe("Own provider"); + expect( + manager.getAgentManagerProviderState().providerDefinitions[registration.id], + ).toMatchObject({ enabled: true }); + expect(warnings.join("\n")).toContain("shadowed"); + expect(launches).toEqual([]); + manager.replacePluginProviders([]); + expect(manager.hasProvider(registration.id)).toBe(true); + } finally { + await manager.shutdown(); + manager.destroy(); + } + }); +}); + +test("model overrides preserve negotiated plugin capabilities and connection shutdown", async () => { + let closed = 0; + const registration: ProviderRegistration = { + id: "model-plugin", + label: "Model plugin", + async connect() { + let listener: ((event: ProviderEvent) => void) | undefined; + return { + version: 1, + capabilities: ["session.persistence"], + async send(input) { + if (input.type === "catalog") + listener?.({ + type: "catalog", + requestId: input.requestId, + catalog: { models: [], modes: [] }, + }); + }, + onEvent(next) { + listener = next; + return () => { + listener = undefined; + }; + }, + async close() { + closed += 1; + }, + }; + }, + }; + const manager = new ProviderSnapshotManager({ + logger: createTestLogger(), + providerOverrides: { + [registration.id]: { models: [{ id: "configured", label: "Configured" }] }, + }, + }); + try { + manager.replacePluginProviders([registration]); + await manager.getProvider({ provider: registration.id, wait: true }); + const client = manager.getAgentManagerProviderState().clients[registration.id]!; + expect(client.capabilities.supportsSessionPersistence).toBe(true); + await manager.shutdown(); + expect(closed).toBe(1); + } finally { + await manager.shutdown(); + manager.destroy(); + } +}); diff --git a/packages/server/src/server/agent/provider-snapshot-manager.ts b/packages/server/src/server/agent/provider-snapshot-manager.ts index 0307dc14be..22a706a279 100644 --- a/packages/server/src/server/agent/provider-snapshot-manager.ts +++ b/packages/server/src/server/agent/provider-snapshot-manager.ts @@ -256,6 +256,7 @@ export class ProviderSnapshotManager { private providerClients: Record; private readonly ownedClients = new Set(); private readonly pluginProviders: PluginAgentClientRegistry; + private pluginProvidersSettled = false; constructor(options: ProviderSnapshotManagerOptions) { this.logger = options.logger; @@ -281,7 +282,6 @@ export class ProviderSnapshotManager { ); this.providerClients = { ...this.extraClients, - ...this.pluginProviders.clients(), } as Record; for (const client of Object.values(this.providerClients)) this.ownedClients.add(client); } @@ -385,17 +385,28 @@ export class ProviderSnapshotManager { return { providerDefinitions, clients }; } + /** Called after built-in and configured plugin startup has completed, including disabled plugins. */ + settlePluginProviders(): void { + if (this.pluginProvidersSettled) return; + this.pluginProvidersSettled = true; + this.warnUnknownProviderOverrides(); + } + + private warnUnknownProviderOverrides(): void { + if (!this.pluginProvidersSettled) return; + for (const [provider, override] of Object.entries(this.providerOverrides ?? {})) { + if (!override.extends && !this.generation.definitions[provider]) { + this.logger.warn({ provider }, "Provider override matches no registered provider"); + } + } + } + replacePluginProviders( registrations: readonly ProviderRegistration[], ): AgentManagerProviderState { for (const registration of registrations) { - if ( - (this.generation.definitions[registration.id] || this.extraClients[registration.id]) && - !this.pluginProviders.has(registration.id) - ) { - throw new Error( - `Plugin provider '${registration.id}' conflicts with a configured provider`, - ); + if (BUILTIN_PROVIDER_IDS.includes(registration.id) || this.extraClients[registration.id]) { + throw new Error(`Plugin provider '${registration.id}' conflicts with a built-in provider`); } } const previousPlugins = this.pluginProviders.definitions(); @@ -405,17 +416,30 @@ export class ProviderSnapshotManager { this.pluginProviders.replace(registrations); const plugins = this.pluginProviders.definitions(); const retiredProviders = Object.keys(previousPlugins).filter( - (provider) => previousPlugins[provider] !== plugins[provider], + (provider) => + previousPlugins[provider] !== plugins[provider] && + !this.providerOverrides?.[provider]?.extends, ); - const definitions = { ...this.generation.definitions }; + const definitions = this.buildRegistry(this.runtimeSettings, this.providerOverrides); const changed = new Set(); - for (const provider of new Set([...Object.keys(previousPlugins), ...Object.keys(plugins)])) { - if (previousPlugins[provider] !== plugins[provider]) changed.add(provider); - delete definitions[provider]; - delete clients[provider]; + for (const provider of new Set([...this.generation.order, ...Object.keys(definitions)])) { + const before = this.generation.definitions[provider]; + const after = definitions[provider]; + const registrationChanged = + previousPlugins[provider] !== plugins[provider] && + !this.providerOverrides?.[provider]?.extends; + if ( + registrationChanged || + !before || + !after || + !isDeepStrictEqual(before.configuration, after.configuration) + ) { + changed.add(provider); + delete clients[provider]; + } else { + definitions[provider] = before; + } } - Object.assign(definitions, plugins); - Object.assign(clients, this.pluginProviders.clients()); for (const client of Object.values(clients)) this.ownedClients.add(client); const generation = this.createGeneration(definitions, this.providerOverrides); const state = this.createAgentManagerState(definitions, clients); @@ -607,7 +631,7 @@ export class ProviderSnapshotManager { definitions[provider] = before; } } - Object.assign(clients, this.extraClients, this.pluginProviders.clients()); + Object.assign(clients, this.extraClients); const generation = this.createGeneration(definitions, providerOverrides); const agentManagerState = this.createAgentManagerState(definitions, clients); return { @@ -631,6 +655,7 @@ export class ProviderSnapshotManager { } this.generation = generation; this.providerClients = clients; + this.warnUnknownProviderOverrides(); for (const [key, catalogs] of this.catalogs) { for (const provider of changed) catalogs.delete(provider); if (catalogs.size === 0) this.catalogs.delete(key); @@ -689,19 +714,13 @@ export class ProviderSnapshotManager { const registry = buildProviderRegistry(this.logger, { runtimeSettings, providerOverrides, + pluginProviders: this.pluginProviders.definitions(), workspaceGitService: this.workspaceGitService, managedProcesses: this.managedProcesses, openCodeBridge: this.openCodeBridge, isDev: this.isDev, }); - for (const [provider, definition] of Object.entries(this.pluginProviders.definitions())) { - if (registry[provider]) { - throw new Error(`Plugin provider '${provider}' conflicts with a configured provider`); - } - registry[provider] = definition; - } - for (const [provider, client] of Object.entries(this.extraClients) as Array< [AgentProvider, AgentClient] >) { diff --git a/packages/server/src/server/bootstrap.ts b/packages/server/src/server/bootstrap.ts index 695d7b25cb..cf7b147929 100644 --- a/packages/server/src/server/bootstrap.ts +++ b/packages/server/src/server/bootstrap.ts @@ -1734,6 +1734,7 @@ export async function createPaseoDaemon( ); pluginRuntime.bindPaseoSessionHost(wsServer); await pluginRuntime.start(); + providerSnapshotManager.settlePluginProviders(); wsServer.beginAcceptingConnections(); relayRuntime = createRelayRuntime({ config: { diff --git a/packages/server/src/server/persisted-config.test.ts b/packages/server/src/server/persisted-config.test.ts index 63f18e2b5e..f443f84865 100644 --- a/packages/server/src/server/persisted-config.test.ts +++ b/packages/server/src/server/persisted-config.test.ts @@ -413,20 +413,24 @@ describe("provider overrides (new format)", () => { expect(parsed.agents?.providers?.claude?.order).toBe(1); }); - test("new provider without extends → error", () => { - const result = PersistedConfigSchema.safeParse({ - agents: { - providers: { - zai: { - label: "ZAI", - }, - }, - }, + test("accepts a plugin override before plugin registration", () => { + const override = { enabled: false, command: ["/opt/agent"], env: { LOGIN: "yes" } }; + const parsed = PersistedConfigSchema.parse({ + agents: { providers: { "plugin-agent": override } }, }); - - expect(result.success).toBe(false); + expect(parsed.agents?.providers?.["plugin-agent"]).toEqual(override); }); + test.each(["plugin.agent", "plugin_agent"])( + "keeps %s outside the config provider ID alphabet", + (id) => { + expect( + PersistedConfigSchema.safeParse({ agents: { providers: { [id]: { enabled: false } } } }) + .success, + ).toBe(false); + }, + ); + test("new provider without label → error", () => { const result = PersistedConfigSchema.safeParse({ agents: { diff --git a/packages/server/src/server/plugins/index.posix.test.ts b/packages/server/src/server/plugins/index.posix.test.ts index 4b76a4af25..544b609df5 100644 --- a/packages/server/src/server/plugins/index.posix.test.ts +++ b/packages/server/src/server/plugins/index.posix.test.ts @@ -1,5 +1,15 @@ import { randomUUID } from "node:crypto"; -import { rename, mkdir, mkdtemp, readFile, readdir, rm, stat, writeFile } from "node:fs/promises"; +import { + cp, + rename, + mkdir, + mkdtemp, + readFile, + readdir, + rm, + stat, + writeFile, +} from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; import { pathToFileURL } from "node:url"; @@ -7,6 +17,7 @@ import pino from "pino"; import { afterEach, describe, expect, it } from "vitest"; import { DaemonConfigStore } from "../daemon-config-store.js"; import { PluginService } from "./index.js"; +import { BuiltinPluginLoader } from "./builtin/index.js"; import { ManagedPluginSources } from "./managed-source.js"; import { startNpmRegistry, @@ -168,6 +179,78 @@ function createPluginSelectivePausedRuntime(pausedPluginId: string) { } describe("PluginService", () => { + it.each([ + { shadow: false, runtime: "subprocess" }, + { shadow: true, runtime: "subprocess" }, + { shadow: false, runtime: "builtin" }, + { shadow: true, runtime: "builtin" }, + ])( + "publishes status and launch metadata through $runtime despite a configured provider (extends: $shadow)", + async ({ shadow, runtime }) => { + const home = await mkdtemp(path.join(tmpdir(), "paseo-plugin-home-")); + roots.push(home); + const directory = await createPlugin( + "provider-status", + `export default function contribute(server) { + server.registerProvider({ id: "plugin-agent", label: "Plugin agent", command: ["agent", "serve"], + async status({ launch }) { return launch.env.LOGIN === "yes" ? { available: true } : { available: false, diagnostic: "Sign in" }; }, + async getCatalogCacheKey({ launch }) { return launch.command + ":" + launch.env.LOGIN; }, + async connect({ launch }) { + if (launch.command !== process.execPath || launch.env.LOGIN !== "no") throw new Error("Unexpected launch"); + return { version: 1, capabilities: [], async send() {}, onEvent() { return () => {}; }, async close() {} }; + }, + }); + return () => {}; + }`, + ); + const store = createStore(home); + await store.patch({ + providers: { + "plugin-agent": shadow + ? { extends: "acp", label: "Own provider", command: ["own-agent"] } + : { command: ["configured-agent"], enabled: false }, + }, + }); + const builtinRoot = path.join(home, "builtins"); + if (runtime === "builtin") { + await mkdir(builtinRoot); + await cp(directory, path.join(builtinRoot, "provider-status"), { recursive: true }); + } + const builtinPlugins = new BuiltinPluginLoader( + builtinRoot, + runtime === "builtin" ? ["provider-status"] : [], + ); + const service = bindTestSessionHost( + new PluginService(pino({ level: "silent" }), store, "0.4.0", { builtinPlugins }), + ); + await service.start(); + try { + if (runtime === "subprocess") await service.installDirectory({ path: directory }); + const [registration] = service.getProviderRegistrations(); + expect(registration).toMatchObject({ + id: "plugin-agent", + command: ["agent", "serve"], + status: expect.any(Function), + }); + const launch = { command: process.execPath, args: [], env: { LOGIN: "no" } }; + expect(await registration!.getCatalogCacheKey!({ scope: "global", launch })).toBe( + process.execPath + ":no", + ); + expect(await registration!.status!({ launch })).toEqual({ + available: false, + diagnostic: "Sign in", + }); + expect( + await registration!.status!({ launch: { ...launch, env: { LOGIN: "yes" } } }), + ).toEqual({ available: true }); + const connection = await registration!.connect({ versions: [1], capabilities: [], launch }); + await connection.close(); + } finally { + await service.stopAllPlugins(); + } + }, + ); + it("resolves a provider icon path to sanitized inline SVG", async () => { const home = await mkdtemp(path.join(tmpdir(), "paseo-plugin-home-")); roots.push(home); diff --git a/packages/server/src/server/plugins/index.ts b/packages/server/src/server/plugins/index.ts index 830d12251a..edf88de4ca 100644 --- a/packages/server/src/server/plugins/index.ts +++ b/packages/server/src/server/plugins/index.ts @@ -45,6 +45,7 @@ interface PluginRuntimePort { discoverUsage: PluginRuntime["discoverUsage"]; connectProvider: PluginRuntime["connectProvider"]; getProviderCatalogCacheKey?: PluginRuntime["getProviderCatalogCacheKey"]; + getProviderStatus?: PluginRuntime["getProviderStatus"]; validatePlugin?(path: string): Promise; startPlugin(pluginId: string, path: string, canPublish: () => boolean): Promise; startBuiltinPlugin?(plugin: BuiltinPlugin): Promise; @@ -602,16 +603,10 @@ export class PluginService { pluginDirectory: string, ): Promise { const metadata = this.runtime.getProviderRegistrations?.(pluginId) ?? []; - const configuredIds = new Set(Object.keys(this.configStore.get().providers)); for (const provider of metadata) { if (BUILTIN_PROVIDER_ID_SET.has(provider.id)) { throw new Error(`Plugin ${pluginId} cannot register builtin provider ID "${provider.id}"`); } - if (configuredIds.has(provider.id)) { - throw new Error( - `Plugin ${pluginId} cannot register configured provider ID "${provider.id}"`, - ); - } if (this.providers.has(provider.id)) { throw new Error(`Plugin ${pluginId} cannot register provider ID "${provider.id}" twice`); } @@ -622,6 +617,14 @@ export class PluginService { id: provider.id, label: provider.label, description: provider.description, + command: provider.command, + status: provider.hasStatus + ? (request) => { + if (!this.runtime.getProviderStatus) + throw new Error("Plugin runtime cannot resolve provider status"); + return this.runtime.getProviderStatus(pluginId, provider.id, request); + } + : undefined, getCatalogCacheKey: provider.hasCatalogCacheKey ? (options) => { if (!this.runtime.getProviderCatalogCacheKey) diff --git a/packages/server/src/server/plugins/plugin-process-protocol.ts b/packages/server/src/server/plugins/plugin-process-protocol.ts index cf1cd2c32a..6ae2cf140f 100644 --- a/packages/server/src/server/plugins/plugin-process-protocol.ts +++ b/packages/server/src/server/plugins/plugin-process-protocol.ts @@ -3,12 +3,19 @@ import type { ProviderCatalogOptions, ProviderEvent, ProviderInput, + ProviderStatusRequest, +} from "@getpaseo/plugin/server/provider"; +import { + ProviderEventSchema, + ProviderInputSchema, + ProviderLaunchSchema, } from "@getpaseo/plugin/server/provider"; -import { ProviderEventSchema, ProviderInputSchema } from "@getpaseo/plugin/server/provider"; import { z } from "zod"; export interface PluginProviderMetadata { hasCatalogCacheKey?: boolean; + hasStatus?: boolean; + command?: readonly [string, ...string[]]; id: string; label: string; description?: string; @@ -22,6 +29,12 @@ export interface PluginUsageSourceMetadata { } export type PluginProcessRequest = + | { + type: "provider.status"; + requestId: string; + providerId: string; + request: ProviderStatusRequest; + } | { type: "initialize"; pluginId: string; @@ -100,6 +113,8 @@ const providerMetadataSchema = z description: z.string().optional(), iconPath: z.string().optional(), hasCatalogCacheKey: z.boolean().optional(), + hasStatus: z.boolean().optional(), + command: z.tuple([z.string().min(1)], z.string()).optional(), }) .strict(); const usageSourceMetadataSchema = z @@ -111,6 +126,7 @@ const usageSourceMetadataSchema = z .strict(); const providerConnectRequestSchema = z .object({ + launch: ProviderLaunchSchema.optional(), versions: z.array(z.number().int().positive()), capabilities: z.array(z.string()), }) @@ -123,6 +139,14 @@ const frameFields = { export const PluginProcessRequestSchema: z.ZodType = z.discriminatedUnion( "type", [ + z + .object({ + type: z.literal("provider.status"), + requestId: z.string().min(1), + providerId: z.string().min(1), + request: z.object({ launch: ProviderLaunchSchema.optional() }).strict(), + }) + .strict(), z .object({ type: z.literal("initialize"), @@ -139,12 +163,19 @@ export const PluginProcessRequestSchema: z.ZodType = z.dis requestId: z.string().min(1), providerId: z.string().min(1), options: z.discriminatedUnion("scope", [ - z.object({ scope: z.literal("global"), force: z.boolean().optional() }).strict(), + z + .object({ + scope: z.literal("global"), + force: z.boolean().optional(), + launch: ProviderLaunchSchema.optional(), + }) + .strict(), z .object({ scope: z.literal("workspace"), cwd: z.string(), force: z.boolean().optional(), + launch: ProviderLaunchSchema.optional(), }) .strict(), ]), diff --git a/packages/server/src/server/plugins/plugin-process.ts b/packages/server/src/server/plugins/plugin-process.ts index 2c371d6ba1..ee1d17310a 100644 --- a/packages/server/src/server/plugins/plugin-process.ts +++ b/packages/server/src/server/plugins/plugin-process.ts @@ -14,6 +14,7 @@ import { ProviderEventSchema, type ProviderConnection, type ProviderRegistration, + ProviderStatusSchema, } from "@getpaseo/plugin/server/provider"; import { createPaseoApi, type PaseoApi } from "@getpaseo/client"; import { DaemonClient } from "@getpaseo/client/internal/daemon-client"; @@ -123,6 +124,14 @@ export function createPluginWorker(options: { ) { throw new Error(`Invalid catalogue key callback for plugin provider ${id}`); } + if (provider.status !== undefined && typeof provider.status !== "function") + throw new Error(`Invalid status callback for plugin provider ${id}`); + if ( + provider.command !== undefined && + (provider.command.length === 0 || + provider.command.some((part) => typeof part !== "string" || !part.trim())) + ) + throw new Error(`Invalid command for plugin provider ${id}`); if (providers.has(id)) throw new Error(`Duplicate plugin provider ID: ${id}`); providers.set(id, { ...provider, id }); } @@ -150,6 +159,8 @@ export function createPluginWorker(options: { description: provider.description, iconPath: provider.icon, hasCatalogCacheKey: provider.getCatalogCacheKey !== undefined, + hasStatus: provider.status !== undefined, + command: provider.command, }; } @@ -351,6 +362,7 @@ export function createPluginWorker(options: { function rejectWhileStopping(message: PluginProcessRequest): void { if ( + message.type === "provider.status" || message.type === "provider.catalog_key" || message.type === "usage.identify" || message.type === "usage.fetch" || @@ -416,6 +428,18 @@ export function createPluginWorker(options: { rejectWhileStopping(message); return; } + if (message.type === "provider.status") { + void (async () => { + const provider = providers.get(message.providerId); + if (!provider || !provider.status) + throw new Error(`Provider has no status capability: ${message.providerId}`); + const output = ProviderStatusSchema.parse(await provider.status(message.request)); + send({ type: "result", requestId: message.requestId, output }); + })().catch((error) => + send({ type: "error", requestId: message.requestId, error: describeError(error) }), + ); + return; + } if (message.type === "provider.catalog_key") { void (async () => { const provider = providers.get(message.providerId); diff --git a/packages/server/src/server/plugins/runtime.ts b/packages/server/src/server/plugins/runtime.ts index 312d4d069a..9db0853718 100644 --- a/packages/server/src/server/plugins/runtime.ts +++ b/packages/server/src/server/plugins/runtime.ts @@ -1,3 +1,8 @@ +import { + ProviderStatusSchema, + type ProviderStatus, + type ProviderStatusRequest, +} from "@getpaseo/plugin/server/provider"; import type { PluginBeforeRequests, PluginLifecycleEvents } from "@getpaseo/plugin/server"; import { validateBeforeRequest, validateBeforeResult } from "./lifecycle/index.js"; import { fork } from "node:child_process"; @@ -413,6 +418,23 @@ export class PluginRuntime { return this.request(loaded, { type: "usage.discover", requestId: randomUUID(), sourceId }); } + async getProviderStatus( + pluginId: string, + providerId: string, + request: ProviderStatusRequest, + ): Promise { + const loaded = this.plugins.get(pluginId); + if (!loaded) throw new Error(`Plugin is not available: ${pluginId}`); + return ProviderStatusSchema.parse( + await this.request(loaded, { + type: "provider.status", + requestId: randomUUID(), + providerId, + request, + }), + ); + } + async connectProvider( pluginId: string, providerId: string, From ad56659a89d00e3154d44ee64041e6df274d3a40 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Wed, 30 Sep 2026 13:46:43 +0100 Subject: [PATCH 017/100] fix(app): resolve plugin provider icons after the snapshot arrives (#5720) Subscribe icon consumers to snapshot SVG metadata so compiler-cached fallbacks update when provider discovery completes. --- .../agent-control-registration.tsx | 7 +- packages/app/src/components/agent-list.tsx | 4 +- .../components/import-session-sheet.test.tsx | 3 +- .../src/components/import-session-sheet.tsx | 15 ++--- packages/app/src/components/model-browser.tsx | 4 +- .../src/components/provider-icon-name.test.ts | 40 +----------- .../app/src/components/provider-icon-name.ts | 24 +------ .../app/src/components/provider-icons.test.ts | 65 +++++++++++++++++-- packages/app/src/components/provider-icons.ts | 36 +++++++--- .../schedules/schedule-form-sheet.tsx | 4 +- .../src/components/schedules/schedule-row.tsx | 6 +- .../src/components/terminal-profile-icon.tsx | 4 +- .../composer/agent-controls/model-sheet.tsx | 12 ++-- packages/app/src/data/provider-icons.ts | 17 +++++ .../app/src/data/providers-snapshot.test.ts | 26 +++----- packages/app/src/data/providers-snapshot.ts | 2 +- .../src/hooks/use-providers-snapshot.test.ts | 7 +- packages/app/src/panels/agent-panel.tsx | 4 +- .../src/panels/provider-subagent-panel.tsx | 4 +- .../app/src/screens/settings/host-page.tsx | 4 +- .../settings/providers-section.test.tsx | 2 +- .../screens/settings/providers-section.tsx | 4 +- packages/app/src/subagents/track.tsx | 11 ++-- 23 files changed, 165 insertions(+), 140 deletions(-) create mode 100644 packages/app/src/data/provider-icons.ts diff --git a/packages/app/src/command-center/agent-control-registration.tsx b/packages/app/src/command-center/agent-control-registration.tsx index 50e61ed9fd..21ce492373 100644 --- a/packages/app/src/command-center/agent-control-registration.tsx +++ b/packages/app/src/command-center/agent-control-registration.tsx @@ -13,7 +13,7 @@ import { PlanModeIcon, ThinkingIcon, } from "@/agent-controls/icons"; -import { getProviderIcon } from "@/components/provider-icons"; +import { useProviderIcons } from "@/components/provider-icons"; import type { ProviderSelectorProvider } from "@/provider-selection/provider-selection"; import { buildAgentControlContributions, @@ -88,6 +88,7 @@ export function useAgentControlCommandCenterActions(input: { const { t } = useTranslation(); const { controls } = input; const { features, models, modes, thinking } = controls; + const getProviderIcon = useProviderIcons(controls.serverId); const actions = useMemo( () => buildAgentControlContributions({ @@ -96,8 +97,7 @@ export function useAgentControlCommandCenterActions(input: { provider: controls.provider ?? null, labels: buildAgentControlContributionLabels(t), icons: { - provider: (provider) => - getCommandCenterIcon(getProviderIcon(provider, controls.serverId)), + provider: (provider) => getCommandCenterIcon(getProviderIcon(provider)), thinking: getCommandCenterIcon(ThinkingIcon), planMode: getCommandCenterIcon(PlanModeIcon), mode: (modeId) => @@ -129,6 +129,7 @@ export function useAgentControlCommandCenterActions(input: { }, }), [ + getProviderIcon, controls.ownerKey, controls.provider, controls.providerDefinitions, diff --git a/packages/app/src/components/agent-list.tsx b/packages/app/src/components/agent-list.tsx index 406b498bfe..7da36742ae 100644 --- a/packages/app/src/components/agent-list.tsx +++ b/packages/app/src/components/agent-list.tsx @@ -18,7 +18,7 @@ import { useTimeAgo } from "@/hooks/use-time-ago"; import { type AggregatedAgent } from "@/hooks/use-aggregated-agents"; import { useSessionStore } from "@/stores/session-store"; import { Archive, ChevronRight } from "lucide-react-native"; -import { getProviderIcon } from "@/components/provider-icons"; +import { useProviderIcon } from "@/components/provider-icons"; import { navigateToAgent } from "@/utils/navigate-to-agent"; import { useArchiveAgent } from "@/hooks/use-archive-agent"; import { HighlightedText } from "@/components/ui/highlighted-text"; @@ -186,7 +186,7 @@ function SessionRow({ const projectName = agent.projectPlacement?.projectName ?? ""; const branch = agent.projectPlacement?.checkout.currentBranch ?? ""; const workspaceName = agent.projectPlacement?.workspaceName ?? ""; - const ProviderIcon = getProviderIcon(agent.provider, agent.serverId); + const ProviderIcon = useProviderIcon(agent.provider, agent.serverId); const pendingPermissionCount = agent.pendingPermissionCount ?? 0; const ranges = useMemo( () => ({ diff --git a/packages/app/src/components/import-session-sheet.test.tsx b/packages/app/src/components/import-session-sheet.test.tsx index 65e32d752c..35c475e824 100644 --- a/packages/app/src/components/import-session-sheet.test.tsx +++ b/packages/app/src/components/import-session-sheet.test.tsx @@ -63,7 +63,8 @@ vi.mock("@/constants/layout", () => ({ })); vi.mock("@/components/provider-icons", () => ({ - getProviderIcon: () => () => null, + useProviderIcon: () => () => null, + useProviderIcons: () => () => () => null, })); vi.mock("lucide-react-native", () => { diff --git a/packages/app/src/components/import-session-sheet.tsx b/packages/app/src/components/import-session-sheet.tsx index c6d13d1a28..3be15c0892 100644 --- a/packages/app/src/components/import-session-sheet.tsx +++ b/packages/app/src/components/import-session-sheet.tsx @@ -13,7 +13,7 @@ import { AdaptiveModalSheet, type SheetHeader } from "@/components/adaptive-moda import { Button } from "@/components/ui/button"; import { LoadingSpinner } from "@/components/ui/loading-spinner"; import { Combobox, ComboboxItem, type ComboboxOption } from "@/components/ui/combobox"; -import { getProviderIcon } from "@/components/provider-icons"; +import { useProviderIcon, useProviderIcons } from "@/components/provider-icons"; import { useTimeAgo } from "@/hooks/use-time-ago"; import { useDebouncedValue } from "@/hooks/use-debounced-value"; import { useProvidersSnapshot } from "@/hooks/use-providers-snapshot"; @@ -309,7 +309,7 @@ function ImportSessionSheetRow({ const { t } = useTranslation(); const title = getSessionTitle(entry); const promptPreview = getPromptPreview(entry); - const ProviderIcon = getProviderIcon(entry.providerId, serverId); + const ProviderIcon = useProviderIcon(entry.providerId, serverId); const accessibilityState = useMemo( () => (disabled ? DISABLED_ACCESSIBILITY_STATE : undefined), [disabled], @@ -559,15 +559,17 @@ export function ImportSessionSheet({ setIsFilterOpen(false); }, []); + const getProviderIcon = useProviderIcons(serverId); + const SelectedProviderIcon = useProviderIcon(selectedProvider, serverId); const filterOptionIcons = useMemo(() => { const map = new Map(); map.set(ALL_FILTER_VALUE, ); for (const provider of filterProviders) { - const ProviderIcon = getProviderIcon(provider, serverId); + const ProviderIcon = getProviderIcon(provider); map.set(provider, ); } return map; - }, [filterProviders, serverId, theme.colors.foregroundMuted]); + }, [filterProviders, getProviderIcon, theme.colors.foregroundMuted]); const renderFilterOption = useCallback( ({ @@ -746,10 +748,7 @@ export function ImportSessionSheet({ {selectedProvider === ALL_FILTER_VALUE ? ( ) : ( - (() => { - const ProviderIcon = getProviderIcon(selectedProvider, serverId); - return ; - })() + )} {selectedProviderLabel} diff --git a/packages/app/src/components/model-browser.tsx b/packages/app/src/components/model-browser.tsx index 9d0c8f8bc3..17924b233d 100644 --- a/packages/app/src/components/model-browser.tsx +++ b/packages/app/src/components/model-browser.tsx @@ -39,7 +39,7 @@ import type { SheetHeader } from "@/components/adaptive-modal-sheet"; import { Button } from "@/components/ui/button"; import { LoadingSpinner } from "@/components/ui/loading-spinner"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; -import { getProviderIcon } from "@/components/provider-icons"; +import { useProviderIcon } from "@/components/provider-icons"; import { useIsCompactFormFactor } from "@/constants/layout"; import { isNative, isWeb } from "@/constants/platform"; import { @@ -223,7 +223,7 @@ export function ModelProviderGlyph({ size: number; tone?: ProviderGlyphTone; }) { - const Icon = getProviderIcon(provider, serverId); + const Icon = useProviderIcon(provider, serverId); const color = tone === "foreground" ? styles.providerIconForeground.color : styles.providerIconMuted.color; return ; diff --git a/packages/app/src/components/provider-icon-name.test.ts b/packages/app/src/components/provider-icon-name.test.ts index de2f8c3798..65c75166a5 100644 --- a/packages/app/src/components/provider-icon-name.test.ts +++ b/packages/app/src/components/provider-icon-name.test.ts @@ -5,7 +5,7 @@ import { TERMINAL_PROFILE_ICON_NAMES, } from "@getpaseo/protocol/provider-icon-names"; import { ACP_PROVIDER_CATALOG } from "@/data/acp-provider-catalog"; -import { replaceProviderSnapshotIcons, resolveProviderIconName } from "./provider-icon-name"; +import { resolveProviderIconName } from "./provider-icon-name"; describe("resolveProviderIconName", () => { it("returns the built-in identifier for known provider ids", () => { @@ -29,43 +29,7 @@ describe("resolveProviderIconName", () => { it("resolves a snapshot SVG for a custom provider", () => { const svg = ''; - replaceProviderSnapshotIcons("server-1", [{ provider: "snapshot-provider", iconSvg: svg }]); - - expect(resolveProviderIconName("snapshot-provider", "server-1")).toEqual({ kind: "svg", svg }); - }); - - it("replaces each host snapshot without leaking icons across hosts", () => { - const secondSvg = ""; - replaceProviderSnapshotIcons("server-1", [ - { provider: "removed-provider", iconSvg: "" }, - { provider: "snapshot-provider", iconSvg: "" }, - { provider: "claude", iconSvg: "" }, - ]); - replaceProviderSnapshotIcons("server-2", [ - { provider: "snapshot-provider", iconSvg: secondSvg }, - ]); - - expect(resolveProviderIconName("snapshot-provider", "server-1")).toEqual({ - kind: "svg", - svg: "", - }); - expect(resolveProviderIconName("snapshot-provider", "server-2")).toEqual({ - kind: "svg", - svg: secondSvg, - }); - expect(resolveProviderIconName("claude", "server-1")).toEqual({ - kind: "builtin", - id: "claude", - }); - - replaceProviderSnapshotIcons("server-1", [{ provider: "snapshot-provider" }]); - - expect(resolveProviderIconName("removed-provider", "server-1")).toEqual({ kind: "bot" }); - expect(resolveProviderIconName("snapshot-provider", "server-1")).toEqual({ kind: "bot" }); - expect(resolveProviderIconName("snapshot-provider", "server-2")).toEqual({ - kind: "svg", - svg: secondSvg, - }); + expect(resolveProviderIconName("snapshot-provider", svg)).toEqual({ kind: "svg", svg }); }); }); diff --git a/packages/app/src/components/provider-icon-name.ts b/packages/app/src/components/provider-icon-name.ts index 699865fd01..38d4344cd1 100644 --- a/packages/app/src/components/provider-icon-name.ts +++ b/packages/app/src/components/provider-icon-name.ts @@ -2,7 +2,6 @@ import { BUILTIN_PROVIDER_ICON_NAMES, KNOWN_PROVIDER_ICON_NAMES, } from "@getpaseo/protocol/provider-icon-names"; -import type { ProviderSnapshotEntry } from "@getpaseo/protocol/agent-types"; export type ProviderIconName = | { kind: "builtin"; id: string } @@ -12,31 +11,10 @@ export type ProviderIconName = const BUILTIN_PROVIDER_IDS = new Set(BUILTIN_PROVIDER_ICON_NAMES); const KNOWN_PROVIDER_IDS = new Set(KNOWN_PROVIDER_ICON_NAMES); -const providerSnapshotIconSvgsByServer = new Map>(); - -export function replaceProviderSnapshotIcons( - serverId: string, - entries: readonly Pick[], -): void { - const icons = new Map(); - for (const entry of entries) { - if (entry.iconSvg) { - icons.set(entry.provider, entry.iconSvg); - } - } - providerSnapshotIconSvgsByServer.set(serverId, icons); -} - -export function resolveProviderIconName( - provider: string, - serverId?: string | null, -): ProviderIconName { +export function resolveProviderIconName(provider: string, iconSvg?: string): ProviderIconName { if (BUILTIN_PROVIDER_IDS.has(provider)) { return { kind: "builtin", id: provider }; } - const iconSvg = serverId - ? providerSnapshotIconSvgsByServer.get(serverId)?.get(provider) - : undefined; if (iconSvg) { return { kind: "svg", svg: iconSvg }; } diff --git a/packages/app/src/components/provider-icons.test.ts b/packages/app/src/components/provider-icons.test.ts index 5235218875..025ff14bef 100644 --- a/packages/app/src/components/provider-icons.test.ts +++ b/packages/app/src/components/provider-icons.test.ts @@ -1,8 +1,12 @@ +// @vitest-environment jsdom +import { act, cleanup, renderHook } from "@testing-library/react"; import { Bot } from "lucide-react-native"; import { SvgXml } from "react-native-svg"; -import { describe, expect, it } from "vitest"; -import { replaceProviderSnapshotIcons } from "./provider-icon-name"; -import { getProviderIcon, type ProviderIconComponent } from "./provider-icons"; +import { afterEach, describe, expect, it } from "vitest"; +import { replaceProviderSnapshotIcons } from "@/data/provider-icons"; +import { useProviderIcon, useProviderIcons, type ProviderIconComponent } from "./provider-icons"; + +afterEach(cleanup); function renderIcon(Component: ProviderIconComponent) { if (typeof Component !== "function") throw new Error("Expected a function component"); @@ -12,12 +16,14 @@ function renderIcon(Component: ProviderIconComponent) { }); } -describe("getProviderIcon", () => { +describe("useProviderIcon", () => { it("renders registered snapshot SVG metadata with the requested size and color", () => { const svg = ''; replaceProviderSnapshotIcons("server-1", [{ provider: "rendered-provider", iconSvg: svg }]); - const rendered = renderIcon(getProviderIcon("rendered-provider", "server-1")); + const rendered = renderIcon( + renderHook(() => useProviderIcon("rendered-provider", "server-1")).result.current, + ); expect(rendered).toMatchObject({ type: SvgXml, @@ -28,6 +34,53 @@ describe("getProviderIcon", () => { it("uses the normal Bot fallback without snapshot SVG metadata", () => { replaceProviderSnapshotIcons("server-1", [{ provider: "plain-provider" }]); - expect(getProviderIcon("plain-provider", "server-1")).toBe(Bot); + expect(renderHook(() => useProviderIcon("plain-provider", "server-1")).result.current).toBe( + Bot, + ); }); }); + +it("updates a mounted consumer when the plugin snapshot arrives after mount", () => { + const serverId = "cold-reload-host"; + const provider = "plugin-provider"; + const svg = ''; + replaceProviderSnapshotIcons(serverId, []); + const { result, unmount } = renderHook(() => useProviderIcon(provider, serverId)); + expect(result.current).toBe(Bot); + act(() => replaceProviderSnapshotIcons(serverId, [{ provider, iconSvg: svg }])); + expect(renderIcon(result.current)).toMatchObject({ + type: SvgXml, + props: { xml: svg, width: 18, height: 18, color: "#123456" }, + }); + unmount(); +}); + +it("keeps host SVGs isolated, updates replacements, and clears removed icons", () => { + const provider = "shared-plugin-provider"; + const firstSvg = ''; + const secondSvg = ''; + replaceProviderSnapshotIcons("first-host", [{ provider, iconSvg: firstSvg }]); + replaceProviderSnapshotIcons("second-host", [{ provider, iconSvg: secondSvg }]); + const first = renderHook(() => useProviderIcon(provider, "first-host")); + const second = renderHook(() => useProviderIcon(provider, "second-host")); + const firstIcon = first.result.current; + expect(renderIcon(firstIcon)).toMatchObject({ props: { xml: firstSvg } }); + expect(renderIcon(second.result.current)).toMatchObject({ props: { xml: secondSvg } }); + act(() => replaceProviderSnapshotIcons("second-host", [{ provider, iconSvg: firstSvg }])); + expect(renderIcon(second.result.current)).toMatchObject({ props: { xml: firstSvg } }); + expect(first.result.current).toBe(firstIcon); + act(() => replaceProviderSnapshotIcons("second-host", [])); + expect(second.result.current).toBe(Bot); + expect(first.result.current).toBe(firstIcon); +}); + +it("updates collection consumers when their host snapshot arrives", () => { + const serverId = "collection-host"; + const provider = "collection-plugin-provider"; + const svg = ''; + replaceProviderSnapshotIcons(serverId, []); + const { result } = renderHook(() => useProviderIcons(serverId)); + expect(result.current(provider)).toBe(Bot); + act(() => replaceProviderSnapshotIcons(serverId, [{ provider, iconSvg: svg }])); + expect(renderIcon(result.current(provider))).toMatchObject({ props: { xml: svg } }); +}); diff --git a/packages/app/src/components/provider-icons.ts b/packages/app/src/components/provider-icons.ts index a66eaafa1d..fec4003705 100644 --- a/packages/app/src/components/provider-icons.ts +++ b/packages/app/src/components/provider-icons.ts @@ -1,5 +1,7 @@ +import { useStore } from "zustand"; +import { providerSnapshotIcons } from "@/data/provider-icons"; import { Bot, PackagePlus } from "lucide-react-native"; -import { createElement, type ComponentType } from "react"; +import { createElement, useCallback, type ComponentType } from "react"; import { SvgXml } from "react-native-svg"; import { ClaudeIcon } from "@/components/icons/claude-icon"; import { CodexIcon } from "@/components/icons/codex-icon"; @@ -34,7 +36,7 @@ const CATALOG_ICON_SVGS = new Map( ); const catalogIconComponents = new Map(); -const snapshotIconComponents = new Map(); +const snapshotIconComponents = new Map(); /** Renders a sanitized SVG string from a host catalog, tinted through `currentColor`. */ export function SvgIcon({ svg, size, color }: ProviderIconProps & { svg: string }) { @@ -63,15 +65,15 @@ function getCatalogProviderIcon(provider: string): ProviderIconComponent { } function getSnapshotProviderIcon(provider: string, svg: string): ProviderIconComponent { - const cached = snapshotIconComponents.get(provider); - if (cached?.svg === svg) return cached.component; + const cached = snapshotIconComponents.get(svg); + if (cached) return cached; const component = createSvgIcon(provider, svg); - snapshotIconComponents.set(provider, { svg, component }); + snapshotIconComponents.set(svg, component); return component; } -export function getProviderIcon(provider: string, serverId?: string | null): ProviderIconComponent { - const name = resolveProviderIconName(provider, serverId); +function resolveProviderIcon(provider: string, svg?: string): ProviderIconComponent { + const name = resolveProviderIconName(provider, svg); if (name.kind === "builtin") { return BUILTIN_PROVIDER_ICONS[name.id]; } @@ -79,7 +81,25 @@ export function getProviderIcon(provider: string, serverId?: string | null): Pro return getCatalogProviderIcon(name.id); } if (name.kind === "svg") { - return getSnapshotProviderIcon(`${serverId}:${provider}`, name.svg); + return getSnapshotProviderIcon(provider, name.svg); } return Bot; } + +export function useProviderIcon(provider: string, serverId?: string | null): ProviderIconComponent { + const svg = useStore(providerSnapshotIcons, (state) => + serverId ? state.get(serverId)?.get(provider) : undefined, + ); + return resolveProviderIcon(provider, svg); +} + +/** Subscribe once for consumers that render a collection of providers. */ +export function useProviderIcons(serverId?: string | null) { + const icons = useStore(providerSnapshotIcons, (state) => + serverId ? state.get(serverId) : undefined, + ); + return useCallback( + (provider: string) => resolveProviderIcon(provider, icons?.get(provider)), + [icons], + ); +} diff --git a/packages/app/src/components/schedules/schedule-form-sheet.tsx b/packages/app/src/components/schedules/schedule-form-sheet.tsx index 2a3cd97c10..230a41e2e9 100644 --- a/packages/app/src/components/schedules/schedule-form-sheet.tsx +++ b/packages/app/src/components/schedules/schedule-form-sheet.tsx @@ -24,7 +24,7 @@ import { HostStatusDotSlot } from "@/components/hosts/host-picker"; import { createControlGeometry, type FieldControlSize } from "@/components/ui/control-geometry"; import { Field, FormTextInput } from "@/components/ui/form-field"; import { Switch } from "@/components/ui/switch"; -import { getProviderIcon } from "@/components/provider-icons"; +import { useProviderIcon } from "@/components/provider-icons"; import { CadenceEditor } from "@/components/schedules/cadence-editor"; import { SelectField, @@ -1026,10 +1026,10 @@ function ProviderGlyph({ provider: string | null; serverId: string | null; }): ReactElement | null { + const Icon = useProviderIcon(provider ?? "", serverId); if (!provider) { return null; } - const Icon = getProviderIcon(provider, serverId); return ; } diff --git a/packages/app/src/components/schedules/schedule-row.tsx b/packages/app/src/components/schedules/schedule-row.tsx index 96099a0ec7..2af4b33428 100644 --- a/packages/app/src/components/schedules/schedule-row.tsx +++ b/packages/app/src/components/schedules/schedule-row.tsx @@ -10,7 +10,7 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { StatusBadge } from "@/components/ui/status-badge"; -import { getProviderIcon } from "@/components/provider-icons"; +import { useProviderIcon } from "@/components/provider-icons"; import { isNative } from "@/constants/platform"; import { useIsCompactFormFactor } from "@/constants/layout"; import { settingsStyles } from "@/styles/settings"; @@ -144,7 +144,7 @@ function ScheduleMeta({ } /** Small provider glyph. Reads the icon color off a StyleSheet object so the - * dynamic component (getProviderIcon) stays compliant without useUnistyles. */ + * dynamic component (useProviderIcon) stays compliant without useUnistyles. */ function ProviderGlyph({ provider, serverId, @@ -152,10 +152,10 @@ function ProviderGlyph({ provider: string | null; serverId: string; }): ReactElement | null { + const Icon = useProviderIcon(provider ?? "", serverId); if (!provider) { return null; } - const Icon = getProviderIcon(provider, serverId); return ; } diff --git a/packages/app/src/components/terminal-profile-icon.tsx b/packages/app/src/components/terminal-profile-icon.tsx index db3a216e7b..66cc00f69b 100644 --- a/packages/app/src/components/terminal-profile-icon.tsx +++ b/packages/app/src/components/terminal-profile-icon.tsx @@ -1,7 +1,7 @@ import type { ReactElement } from "react"; import { SquareTerminal } from "lucide-react-native"; import { withUnistyles } from "react-native-unistyles"; -import { getProviderIcon } from "@/components/provider-icons"; +import { useProviderIcon } from "@/components/provider-icons"; import type { Theme } from "@/styles/theme"; const mutedColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted }); @@ -17,7 +17,7 @@ function ProviderProfileIcon({ size: number; color?: string; }) { - const Icon = getProviderIcon(iconKey); + const Icon = useProviderIcon(iconKey); return ; } diff --git a/packages/app/src/composer/agent-controls/model-sheet.tsx b/packages/app/src/composer/agent-controls/model-sheet.tsx index 1bebc8a5ac..bca5a250bf 100644 --- a/packages/app/src/composer/agent-controls/model-sheet.tsx +++ b/packages/app/src/composer/agent-controls/model-sheet.tsx @@ -2,12 +2,11 @@ import { useCallback, useMemo, useState, type ReactNode } from "react"; import { useTranslation } from "react-i18next"; import { Keyboard, ScrollView, Text, View, type PressableStateCallbackType } from "react-native"; import { StyleSheet } from "react-native-unistyles"; -import { Bot } from "lucide-react-native"; import type { AgentProvider } from "@getpaseo/protocol/agent-types"; import type { AgentProfilePicker, AgentProfileSeed } from "@/agent-profiles"; import { AdaptiveModalSheet } from "@/components/adaptive-modal-sheet"; import { ComboboxTrigger } from "@/components/ui/combobox-trigger"; -import { getProviderIcon } from "@/components/provider-icons"; +import { useProviderIcon } from "@/components/provider-icons"; import { ModelBrowser, useModelBrowser } from "@/components/model-browser"; import { resolveModelBrowserScrolling } from "@/components/model-browser-view"; import { AgentControlTrigger } from "@/composer/agent-controls/control"; @@ -130,9 +129,8 @@ export function CompactModelSheet({ profiles, serverId, }); - const ProviderIcon = - selectedProvider.trim().length > 0 ? getProviderIcon(selectedProvider, serverId) : null; - const ModelIcon = ProviderIcon ?? Bot; + const ModelIcon = useProviderIcon(selectedProvider, serverId); + const hasSelectedProvider = selectedProvider.trim().length > 0; const rootHeader = useMemo( () => ({ ...rootBrowser.header, @@ -277,9 +275,9 @@ export function CompactModelSheet({ testID="combined-model-selector" chevron={null} > - {ProviderIcon ? ( + {hasSelectedProvider ? ( - + ) : null} diff --git a/packages/app/src/data/provider-icons.ts b/packages/app/src/data/provider-icons.ts new file mode 100644 index 0000000000..9556be7ab2 --- /dev/null +++ b/packages/app/src/data/provider-icons.ts @@ -0,0 +1,17 @@ +import type { ProviderSnapshotEntry } from "@getpaseo/protocol/agent-types"; +import { createStore } from "zustand/vanilla"; + +export const providerSnapshotIcons = createStore>>( + () => new Map(), +); + +export function replaceProviderSnapshotIcons( + serverId: string, + entries: readonly Pick[], +): void { + const icons = new Map(); + for (const entry of entries) { + if (entry.iconSvg) icons.set(entry.provider, entry.iconSvg); + } + providerSnapshotIcons.setState((previous) => new Map(previous).set(serverId, icons), true); +} diff --git a/packages/app/src/data/providers-snapshot.test.ts b/packages/app/src/data/providers-snapshot.test.ts index 56cf80653a..c69eff9225 100644 --- a/packages/app/src/data/providers-snapshot.test.ts +++ b/packages/app/src/data/providers-snapshot.test.ts @@ -5,10 +5,7 @@ import type { GetProvidersSnapshotResponseMessage } from "@getpaseo/protocol/mes import { compactProviderSnapshot } from "@getpaseo/protocol/provider-snapshot-codec"; import { applyProvidersSnapshotUpdate } from "./push-router"; import { createProviderSnapshotCache } from "./provider-snapshot-cache"; -import { - resolveProviderIconName, - replaceProviderSnapshotIcons, -} from "@/components/provider-icon-name"; +import { providerSnapshotIcons, replaceProviderSnapshotIcons } from "@/data/provider-icons"; import { describe, expect, it } from "vitest"; import { fetchProvidersSnapshot, @@ -143,10 +140,9 @@ describe("provider fetch with runtime abort signals", () => { expect(result.entries).toEqual(entries); expect(queryClient.getQueryData(queryKey)).toEqual(result); expect((await cache.read(serverId, snapshot.cwd))?.entries).toEqual(entries); - expect(resolveProviderIconName("test-provider", serverId)).toEqual({ - kind: "svg", - svg: entries[0].iconSvg, - }); + expect(providerSnapshotIcons.getState().get(serverId)?.get("test-provider")).toEqual( + entries[0].iconSvg, + ); } finally { queryClient.clear(); replaceProviderSnapshotIcons(serverId, []); @@ -203,10 +199,9 @@ describe.each([ ).rejects.toBeInstanceOf(CancelledError); expect(controller.signal.aborted).toBe(true); expect(queryClient.getQueryData(queryKey)).toEqual(previous); - expect(resolveProviderIconName("test-provider", serverId)).toEqual({ - kind: "svg", - svg: "previous-icon", - }); + expect(providerSnapshotIcons.getState().get(serverId)?.get("test-provider")).toEqual( + "previous-icon", + ); expect((await cache.readHash(serverId, "test-hash"))?.entries ?? null).toEqual( hasBody ? entries : null, ); @@ -287,10 +282,9 @@ it("keeps a replacement query when the cancelled native fetch finishes late", as expect(queryClient.getQueryData(queryKey)).toEqual(replacement); expect(replacement.entries).toEqual(replacementEntries); expect((await cache.read(serverId, snapshot.cwd))?.entries).toEqual(replacementEntries); - expect(resolveProviderIconName("test-provider", serverId)).toEqual({ - kind: "svg", - svg: "replacement-icon", - }); + expect(providerSnapshotIcons.getState().get(serverId)?.get("test-provider")).toEqual( + "replacement-icon", + ); } finally { finishResponse(); queryClient.clear(); diff --git a/packages/app/src/data/providers-snapshot.ts b/packages/app/src/data/providers-snapshot.ts index fadaf5ee77..68b389fe15 100644 --- a/packages/app/src/data/providers-snapshot.ts +++ b/packages/app/src/data/providers-snapshot.ts @@ -7,7 +7,7 @@ import { type ProviderSnapshotCache, } from "./provider-snapshot-cache"; import { queryClient as singletonQueryClient } from "./query-client"; -import { replaceProviderSnapshotIcons } from "@/components/provider-icon-name"; +import { replaceProviderSnapshotIcons } from "@/data/provider-icons"; import { agentCommandsQueryRoot } from "@/hooks/agent-commands-query"; import type { AgentProvider } from "@getpaseo/protocol/agent-types"; import { normalizeWorkspacePath } from "@/utils/workspace-identity"; diff --git a/packages/app/src/hooks/use-providers-snapshot.test.ts b/packages/app/src/hooks/use-providers-snapshot.test.ts index 83f62b4812..2f3c90b340 100644 --- a/packages/app/src/hooks/use-providers-snapshot.test.ts +++ b/packages/app/src/hooks/use-providers-snapshot.test.ts @@ -9,7 +9,7 @@ import { } from "@getpaseo/protocol/provider-snapshot-codec"; import type { CachedProviderSnapshot, ProviderSnapshotCache } from "@/data/provider-snapshot-cache"; import { draftAgentCommandsQueryKey } from "@/hooks/agent-commands-query"; -import { resolveProviderIconName } from "@/components/provider-icon-name"; +import { providerSnapshotIcons } from "@/data/provider-icons"; import { applyProvidersSnapshotUpdate, type ProvidersSnapshotUpdate } from "@/data/push-router"; import { fetchProvidersSnapshot, @@ -141,10 +141,9 @@ describe("fetchProvidersSnapshot", () => { await fetchProvidersSnapshot({ client, serverId, cwd: null, cache: createCache() }); - expect(resolveProviderIconName("snapshot-icon-provider", serverId)).toEqual({ - kind: "svg", + expect(providerSnapshotIcons.getState().get(serverId)?.get("snapshot-icon-provider")).toEqual( svg, - }); + ); }); it("sends no cwd for the home scope", async () => { diff --git a/packages/app/src/panels/agent-panel.tsx b/packages/app/src/panels/agent-panel.tsx index c0669972bf..2d8a605c3f 100644 --- a/packages/app/src/panels/agent-panel.tsx +++ b/packages/app/src/panels/agent-panel.tsx @@ -34,7 +34,7 @@ import { } from "@/composer/pill-styles"; import { getActiveMessageSubmissions } from "@/composer/submission/model"; import { RewindComposerRestoreProvider } from "@/components/rewind/composer-restore"; -import { getProviderIcon } from "@/components/provider-icons"; +import { useProviderIcon } from "@/components/provider-icons"; import { useToastHost, type ToastApi, type ToastState } from "@/components/toast-host"; import type { WorkspaceComposerAttachment } from "@/attachments/types"; import { useWorkspaceAttachmentScopeKey } from "@/attachments/workspace-attachments-store"; @@ -343,7 +343,7 @@ function useAgentPanelDescriptor( ); const provider = descriptorState.provider; const label = resolveWorkspaceAgentTabLabel(descriptorState.title); - const icon = getProviderIcon(provider, context.serverId); + const icon = useProviderIcon(provider, context.serverId); return { label: label ?? "", diff --git a/packages/app/src/panels/provider-subagent-panel.tsx b/packages/app/src/panels/provider-subagent-panel.tsx index 8fc18eb0e3..6c2d482403 100644 --- a/packages/app/src/panels/provider-subagent-panel.tsx +++ b/packages/app/src/panels/provider-subagent-panel.tsx @@ -4,7 +4,7 @@ import { StyleSheet } from "react-native-unistyles"; import invariant from "tiny-invariant"; import { useShallow } from "zustand/react/shallow"; import { AgentStreamView } from "@/agent-stream/view"; -import { getProviderIcon } from "@/components/provider-icons"; +import { useProviderIcon } from "@/components/provider-icons"; import { resolveComposerTrackControlClearance, resolveComposerTrackTailClearance, @@ -97,7 +97,7 @@ function useProviderSubagentDescriptor( subagentType && subagentType !== label ? `${subagentType} · ${providerLabel}` : providerLabel, tooltip: label, titleState: descriptor ? "ready" : "loading", - icon: getProviderIcon(provider, context.serverId), + icon: useProviderIcon(provider, context.serverId), statusBucket: descriptor ? deriveSidebarStateBucket({ status: providerSubagentLifecycleStatus(descriptor.status), diff --git a/packages/app/src/screens/settings/host-page.tsx b/packages/app/src/screens/settings/host-page.tsx index f7b27f820f..6b308547a1 100644 --- a/packages/app/src/screens/settings/host-page.tsx +++ b/packages/app/src/screens/settings/host-page.tsx @@ -63,7 +63,7 @@ import { formatConnectionStatus, getConnectionStatusTone } from "@/utils/daemons import { formatLatency } from "@/utils/latency"; import { ICON_SIZE } from "@/styles/theme"; import type { Theme } from "@/styles/theme"; -import { getProviderIcon } from "@/components/provider-icons"; +import { useProviderIcon } from "@/components/provider-icons"; import { BrowserToolsOptInCard } from "./browser-tools-card"; import { restartDaemonFromSettings, updateDaemonFromSettings } from "./daemon-lifecycle"; @@ -90,7 +90,7 @@ interface DynamicProviderIconProps { } function DynamicProviderIcon({ iconKey, size, color = "" }: DynamicProviderIconProps) { - const Icon = getProviderIcon(iconKey); + const Icon = useProviderIcon(iconKey); return ; } diff --git a/packages/app/src/screens/settings/providers-section.test.tsx b/packages/app/src/screens/settings/providers-section.test.tsx index 8e8ef4a8a4..77aaa4d8ec 100644 --- a/packages/app/src/screens/settings/providers-section.test.tsx +++ b/packages/app/src/screens/settings/providers-section.test.tsx @@ -233,7 +233,7 @@ vi.mock("@/components/ui/dropdown-menu", () => ({ })); vi.mock("@/components/provider-icons", () => ({ - getProviderIcon: (provider: string) => () => + useProviderIcon: (provider: string) => () => React.createElement("span", { "data-icon": `provider-${provider}` }), })); diff --git a/packages/app/src/screens/settings/providers-section.tsx b/packages/app/src/screens/settings/providers-section.tsx index 69e5caadac..4495acba83 100644 --- a/packages/app/src/screens/settings/providers-section.tsx +++ b/packages/app/src/screens/settings/providers-section.tsx @@ -22,7 +22,7 @@ import { type AcpProviderCatalogItem, } from "@/hooks/use-acp-provider-catalog"; import { ProviderCatalogList } from "@/components/provider-catalog-list"; -import { getProviderIcon } from "@/components/provider-icons"; +import { useProviderIcon } from "@/components/provider-icons"; import { LoadingSpinner } from "@/components/ui/loading-spinner"; import { Switch } from "@/components/ui/switch"; import { @@ -183,7 +183,7 @@ function ProviderRow({ const { t } = useTranslation(); const { theme } = useUnistyles(); const isCompact = useIsCompactFormFactor(); - const ProviderIcon = getProviderIcon(def.id, serverId); + const ProviderIcon = useProviderIcon(def.id, serverId); const providerError = enabled && entry.status === "error" && diff --git a/packages/app/src/subagents/track.tsx b/packages/app/src/subagents/track.tsx index e042322d9a..953c13e0c6 100644 --- a/packages/app/src/subagents/track.tsx +++ b/packages/app/src/subagents/track.tsx @@ -1,9 +1,9 @@ -import { useCallback, useMemo, type ReactElement } from "react"; +import { useCallback, type ReactElement } from "react"; import { Pressable, Text, View } from "react-native"; import { useTranslation } from "react-i18next"; import { Archive, Unlink } from "lucide-react-native"; import { StyleSheet, withUnistyles } from "react-native-unistyles"; -import { getProviderIcon } from "@/components/provider-icons"; +import { useProviderIcon } from "@/components/provider-icons"; import { ComposerTrackActions, ComposerTrackPill, ComposerTrackRow } from "@/composer/tracks"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { useIsCompactFormFactor } from "@/constants/layout"; @@ -45,13 +45,14 @@ const IDLE_ARCHIVE_FINISHED_STATUS: ArchiveFinishedStatus = { kind: "idle" }; /** Leading and action glyphs share one size so rows keep a single icon column. */ const ROW_ICON_SIZE = 14; -function buildRowPresentation(row: SubagentRow, serverId: string): WorkspaceTabPresentation { +function useRowPresentation(row: SubagentRow, serverId: string): WorkspaceTabPresentation { + const icon = useProviderIcon(row.provider, serverId); const data = buildSubagentRowPresentationData(row); return { ...data, tooltip: data.label, modified: false, - icon: getProviderIcon(row.provider, serverId), + icon, }; } @@ -185,7 +186,7 @@ function SubagentsTrackRow({ }: SubagentsTrackRowProps): ReactElement { const { t } = useTranslation(); const isCompact = useIsCompactFormFactor(); - const presentation = useMemo(() => buildRowPresentation(row, serverId), [row, serverId]); + const presentation = useRowPresentation(row, serverId); const displayLabel = presentation.titleState === "loading" ? t("common.states.loading") : presentation.label; const handlePress = useCallback(() => { From 2553ab4d8254b0c36a1cb56da4aad0fbca21b53c Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Wed, 30 Sep 2026 13:56:14 +0100 Subject: [PATCH 018/100] fix(app): expose terminal controls on native tablets (#5712) Wide native panes previously followed the compact-layout gate, hiding terminal keys and skipping software-keyboard clearance. Keep bar eligibility tied to native touch while sizing rows from the available pane width. Co-authored-by: Leandro Ardissone --- .../native-terminal-wide-keybar.android.ad | 24 +++++++ .../native-terminal-wide-keybar.ios.ad | 17 +++++ .../native-terminal-wide-paste.ios.yaml | 40 ++++++++++++ .../native-terminal-wide-preconditions.md | 18 ++++++ packages/app/src/components/terminal-pane.tsx | 62 +++++++++++++++---- .../runtime/terminal-virtual-keyboard.test.ts | 36 +++++++++++ .../runtime/terminal-virtual-keyboard.ts | 62 +++++++++++++++++++ 7 files changed, 248 insertions(+), 11 deletions(-) create mode 100644 packages/app/e2e/mobile/agent-device/native-terminal-wide-keybar.android.ad create mode 100644 packages/app/e2e/mobile/agent-device/native-terminal-wide-keybar.ios.ad create mode 100644 packages/app/e2e/mobile/agent-device/native-terminal-wide-paste.ios.yaml create mode 100644 packages/app/e2e/mobile/agent-device/native-terminal-wide-preconditions.md diff --git a/packages/app/e2e/mobile/agent-device/native-terminal-wide-keybar.android.ad b/packages/app/e2e/mobile/agent-device/native-terminal-wide-keybar.android.ad new file mode 100644 index 0000000000..af7e25995e --- /dev/null +++ b/packages/app/e2e/mobile/agent-device/native-terminal-wide-keybar.android.ad @@ -0,0 +1,24 @@ +context platform=android timeout=90000 retries=1 +env APP_ID=sh.paseo.debug + +open "${APP_ID}" +wait "id=\"workspace-new-tab-button\"" 45000 +press "id=\"workspace-new-tab-button\"" +wait "id=\"workspace-new-tab-menu-terminal\"" 10000 +press "id=\"workspace-new-tab-menu-terminal\"" +wait "id=\"terminal-key-esc\"" 10000 +is visible "id=\"terminal-virtual-keyboard-native-wide\"" +is visible "id=\"terminal-key-esc\"" +is visible "id=\"terminal-key-ctrl\"" +is visible "id=\"terminal-key-enter\"" +is visible "id=\"terminal-paste\"" +press "id=\"terminal-keyboard-toggle\"" +type "printf '__AD_%s__\\n' NATIVE_WIDE_KEY_OK" --delay-ms 0 +press "id=\"terminal-key-enter\"" +wait "text" "__AD_NATIVE_WIDE_KEY_OK__" 10000 +press "id=\"terminal-key-ctrl\"" +type "c" --delay-ms 0 +type "printf '__AD_%s__\\n' MODIFIER_RESET_OK" --delay-ms 0 +press "id=\"terminal-key-enter\"" +wait "text" "__AD_MODIFIER_RESET_OK__" 10000 +close diff --git a/packages/app/e2e/mobile/agent-device/native-terminal-wide-keybar.ios.ad b/packages/app/e2e/mobile/agent-device/native-terminal-wide-keybar.ios.ad new file mode 100644 index 0000000000..31c4bba393 --- /dev/null +++ b/packages/app/e2e/mobile/agent-device/native-terminal-wide-keybar.ios.ad @@ -0,0 +1,17 @@ +context platform=ios timeout=90000 retries=1 +env APP_ID=sh.paseo.debug + +open "${APP_ID}" +wait "id=\"workspace-new-tab-button\"" 30000 +press "id=\"workspace-new-tab-button\"" +wait "id=\"workspace-new-tab-menu-terminal\"" 10000 +press "id=\"workspace-new-tab-menu-terminal\"" +wait "id=\"terminal-virtual-keyboard-native-wide\"" 10000 +is visible "id=\"terminal-key-esc\"" +is visible "id=\"terminal-key-enter\"" +is visible "id=\"terminal-paste\"" +press "id=\"terminal-keyboard-toggle\"" +type "printf '__AD_IOS_%s__\\n' KEY_OK" --delay-ms 0 +press "id=\"terminal-key-enter\"" +wait "text" "__AD_IOS_KEY_OK__" 10000 +close diff --git a/packages/app/e2e/mobile/agent-device/native-terminal-wide-paste.ios.yaml b/packages/app/e2e/mobile/agent-device/native-terminal-wide-paste.ios.yaml new file mode 100644 index 0000000000..2cf92faf3c --- /dev/null +++ b/packages/app/e2e/mobile/agent-device/native-terminal-wide-paste.ios.yaml @@ -0,0 +1,40 @@ +appId: sh.paseo.debug +--- +- launchApp +- runFlow: + when: + visible: ".*getRegistrationInfoAsync.*" + commands: + - tapOn: ".*getRegistrationInfoAsync.*" + - tapOn: Dismiss +- assertNotVisible: ".*getRegistrationInfoAsync.*" +- extendedWaitUntil: + visible: + id: workspace-new-tab-button + timeout: 30000 +- tapOn: + id: workspace-new-tab-button +- tapOn: + id: workspace-new-tab-menu-terminal +- extendedWaitUntil: + visible: + id: terminal-virtual-keyboard-native-wide + timeout: 10000 +- assertVisible: + id: terminal-key-esc +- assertVisible: + id: terminal-key-enter +- assertVisible: + id: terminal-paste +- tapOn: + id: terminal-paste +- runFlow: + when: + visible: Allow Paste + commands: + - tapOn: Allow Paste +- tapOn: + id: terminal-key-enter +- extendedWaitUntil: + visible: __AD_IOS_PASTE_OK__ + timeout: 10000 diff --git a/packages/app/e2e/mobile/agent-device/native-terminal-wide-preconditions.md b/packages/app/e2e/mobile/agent-device/native-terminal-wide-preconditions.md new file mode 100644 index 0000000000..c19cedaaf9 --- /dev/null +++ b/packages/app/e2e/mobile/agent-device/native-terminal-wide-preconditions.md @@ -0,0 +1,18 @@ +# Native wide terminal device flows + +Run these against a connected task-owned tablet with the current development app, Metro bundle, and a workspace whose terminal opens successfully. The flows assert `terminal-virtual-keyboard-native-wide`, which is emitted only for a native noncompact window. That assertion makes a compact phone fail before any PTY success marker. A landscape phone that reaches the noncompact breakpoint is excluded by the explicit tablet `--device` argument; do not rely on the runner's default device. + +Android Pixel Tablet API 36.1: + +```sh +agent-device replay packages/app/e2e/mobile/agent-device/native-terminal-wide-keybar.android.ad --platform android --device paseo-tablet-p1-api36 +``` + +iPad Pro 11-inch (M5), iOS 26.5: from the Namespace Mac checkout, prefill the simulator clipboard with the command below, then run the Maestro compatibility flow. Its `runFlow.when.visible` presses **Allow Paste** only if the iOS sheet appears; every other action and output assertion remains strict. The clipboard payload contains `PASTE_OK`, while only executed PTY output contains `__AD_IOS_PASTE_OK__`. + +```sh +printf '%s' "printf '__AD_IOS_%s__\\n' PASTE_OK" | xcrun simctl pbcopy E49C88C3-9A60-4715-B63A-881498D1991A +agent-device replay packages/app/e2e/mobile/agent-device/native-terminal-wide-paste.ios.yaml --maestro --platform ios --device 'iPad Pro 11-inch (M5)' +``` + +The unsigned simulator build used for phase 1 raises a known `getRegistrationInfoAsync` entitlement overlay on launch. The flow dismisses only that exact development error and asserts it is gone before touching terminal controls; another overlay still fails the flow. `native-terminal-wide-keybar.ios.ad` separately proves that the wide iPad bar sends Enter to the PTY. It needs no clipboard permission. The Maestro flow above is the paste gate for both first-run and already-granted states. diff --git a/packages/app/src/components/terminal-pane.tsx b/packages/app/src/components/terminal-pane.tsx index 9249463197..31c36b607c 100644 --- a/packages/app/src/components/terminal-pane.tsx +++ b/packages/app/src/components/terminal-pane.tsx @@ -4,7 +4,13 @@ import { LoadingSpinner } from "@/components/ui/loading-spinner"; import { useRetainedPanelActive } from "@/components/retained-panel"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import * as Clipboard from "expo-clipboard"; -import { Pressable, Text, View, type PressableStateCallbackType } from "react-native"; +import { + Pressable, + Text, + View, + type LayoutChangeEvent, + type PressableStateCallbackType, +} from "react-native"; import Animated, { runOnJS, useAnimatedReaction } from "react-native-reanimated"; import { StyleSheet, useUnistyles } from "react-native-unistyles"; import { Keyboard as KeyboardIcon, KeyboardOff as KeyboardOffIcon } from "lucide-react-native"; @@ -31,9 +37,10 @@ import { } from "@/terminal/runtime/terminal-key-dispatch"; import { getTerminalVirtualKeyboardControlId, + resolveTerminalVirtualKeyboardRows, + shouldShowTerminalVirtualKeyBar, shouldShowTerminalFloatingCopyAction, shouldShowTerminalPasteAction, - TERMINAL_VIRTUAL_KEYBOARD_ROWS, type TerminalVirtualKeyboardControl, } from "@/terminal/runtime/terminal-virtual-keyboard"; import { pasteTerminalClipboard } from "@/terminal/runtime/terminal-paste"; @@ -93,6 +100,7 @@ interface TerminalPaneProps { const TERMINAL_REFIT_DELAYS_MS = [0, 48, 144, 320]; const TERMINAL_RESIZE_DEBOUNCE_MS = 100; +const TERMINAL_KEY_BAR_MAX_WIDTH = 880; const MODIFIER_LABELS = { ctrl: "Ctrl", @@ -138,8 +146,16 @@ function ModifierButton({ modifier, active, onToggle }: ModifierButtonProps) { () => [styles.keyButtonText, active && styles.keyButtonTextActive], [active], ); + const accessibilityState = useMemo(() => ({ selected: active }), [active]); return ( - + {MODIFIER_LABELS[modifier]} ); @@ -162,7 +178,13 @@ function VirtualKeyButton({ id, label, keyValue, onSend }: VirtualKeyButtonProps [], ); return ( - + {label} ); @@ -222,12 +244,18 @@ export function TerminalPane({ return trimmed.length > 0 ? trimmed : undefined; }, [settings.monoFontFamily]); const isMobile = useIsCompactFormFactor(); + const showVirtualKeyBar = shouldShowTerminalVirtualKeyBar({ isNative, isCompact: isMobile }); + const [keyBarWidth, setKeyBarWidth] = useState(0); + const virtualKeyboardRows = resolveTerminalVirtualKeyboardRows({ + isCompact: isMobile, + availableWidth: keyBarWidth, + }); const mobileView = usePanelStore((state) => state.mobilePanel.target); const showMobileAgentList = usePanelStore((state) => state.showMobileAgentList); const swipeGesturesEnabled = isMobile; const { shift: keyboardShift, style: keyboardPaddingStyle } = useKeyboardShiftStyle({ mode: "padding", - enabled: isMobile, + enabled: isNative || isMobile, }); const [keyboardInset, setKeyboardInset] = useState(0); const [isKeyboardVisible, setIsKeyboardVisible] = useState(false); @@ -292,7 +320,7 @@ export function TerminalPane({ }, [terminalId]); const refreshClipboardAvailability = useCallback(async () => { - if (!isMobile) { + if (!showVirtualKeyBar) { setHasClipboardText(false); return; } @@ -302,7 +330,7 @@ export function TerminalPane({ } catch { setHasClipboardText(false); } - }, [isMobile]); + }, [showVirtualKeyBar]); useEffect(() => { void refreshClipboardAvailability(); @@ -436,7 +464,7 @@ export function TerminalPane({ const handleKeyboardChange = useCallback( (nextShift: number) => { - setKeyboardInset(isMobile ? nextShift : 0); + setKeyboardInset(isNative || isMobile ? nextShift : 0); setIsKeyboardVisible(nextShift > 0); pulseKeyboardRefits(); }, @@ -955,6 +983,11 @@ export function TerminalPane({ [keyboardPaddingStyle], ); + const handleKeyBarLayout = useCallback((event: LayoutChangeEvent) => { + const width = event.nativeEvent.layout.width; + if (width > 0) setKeyBarWidth(width); + }, []); + const handleSwipeRight = useCallback(() => { if (!swipeGesturesEnabled) return; emulatorRef.current?.blur(); @@ -1099,10 +1132,14 @@ export function TerminalPane({ ) : null} - {isMobile ? ( + {showVirtualKeyBar ? ( - - {TERMINAL_VIRTUAL_KEYBOARD_ROWS.map((row) => ( + + {virtualKeyboardRows.map((row) => ( ({ }, keyboardRows: { gap: theme.spacing[1], + width: "100%", + maxWidth: TERMINAL_KEY_BAR_MAX_WIDTH, + alignSelf: "center", }, keyboardRow: { flexDirection: "row", diff --git a/packages/app/src/terminal/runtime/terminal-virtual-keyboard.test.ts b/packages/app/src/terminal/runtime/terminal-virtual-keyboard.test.ts index b8541bcfe5..d2ef76fefd 100644 --- a/packages/app/src/terminal/runtime/terminal-virtual-keyboard.test.ts +++ b/packages/app/src/terminal/runtime/terminal-virtual-keyboard.test.ts @@ -2,9 +2,13 @@ import { describe, expect, it } from "vitest"; import { getTerminalVirtualKeyboardControlId, + resolveTerminalVirtualKeyboardRows, + shouldShowTerminalVirtualKeyBar, shouldShowTerminalFloatingCopyAction, shouldShowTerminalPasteAction, TERMINAL_VIRTUAL_KEYBOARD_ROWS, + TERMINAL_VIRTUAL_KEYBOARD_THREE_COLUMN_ROWS, + TERMINAL_VIRTUAL_KEYBOARD_WIDE_ROWS, type TerminalVirtualKeyboardControl, } from "./terminal-virtual-keyboard"; @@ -41,6 +45,38 @@ function controlsByType( } describe("terminal virtual keyboard policy", () => { + it("keeps terminal controls on wide native touch screens with either IME state", () => { + expect(shouldShowTerminalVirtualKeyBar({ isNative: true, isCompact: false })).toBe(true); + expect(shouldShowTerminalVirtualKeyBar({ isNative: true, isCompact: true })).toBe(true); + expect(shouldShowTerminalVirtualKeyBar({ isNative: false, isCompact: true })).toBe(true); + expect(shouldShowTerminalVirtualKeyBar({ isNative: false, isCompact: false })).toBe(false); + }); + + it("selects the same controls in narrow and wide row arrangements", () => { + const ids = (rows: readonly (readonly TerminalVirtualKeyboardControl[])[]) => + rows.flatMap((row) => row.map(getTerminalVirtualKeyboardControlId)); + expect([...ids(TERMINAL_VIRTUAL_KEYBOARD_WIDE_ROWS)].sort()).toEqual( + [...ids(TERMINAL_VIRTUAL_KEYBOARD_ROWS)].sort(), + ); + expect([...ids(TERMINAL_VIRTUAL_KEYBOARD_THREE_COLUMN_ROWS)].sort()).toEqual( + [...ids(TERMINAL_VIRTUAL_KEYBOARD_ROWS)].sort(), + ); + expect(resolveTerminalVirtualKeyboardRows({ isCompact: false, availableWidth: 0 })).toBe( + TERMINAL_VIRTUAL_KEYBOARD_ROWS, + ); + expect(resolveTerminalVirtualKeyboardRows({ isCompact: false, availableWidth: 515 })).toBe( + TERMINAL_VIRTUAL_KEYBOARD_ROWS, + ); + expect(resolveTerminalVirtualKeyboardRows({ isCompact: false, availableWidth: 160 })).toBe( + TERMINAL_VIRTUAL_KEYBOARD_THREE_COLUMN_ROWS, + ); + expect(resolveTerminalVirtualKeyboardRows({ isCompact: false, availableWidth: 698 })).toBe( + TERMINAL_VIRTUAL_KEYBOARD_WIDE_ROWS, + ); + expect(resolveTerminalVirtualKeyboardRows({ isCompact: true, availableWidth: 700 })).toBe( + TERMINAL_VIRTUAL_KEYBOARD_ROWS, + ); + }); it("does not expose redundant Space or Backspace controls", () => { expect(controlIds()).toEqual([ "terminal-key-esc", diff --git a/packages/app/src/terminal/runtime/terminal-virtual-keyboard.ts b/packages/app/src/terminal/runtime/terminal-virtual-keyboard.ts index f931881bce..3dd244fa95 100644 --- a/packages/app/src/terminal/runtime/terminal-virtual-keyboard.ts +++ b/packages/app/src/terminal/runtime/terminal-virtual-keyboard.ts @@ -41,6 +41,68 @@ export const TERMINAL_VIRTUAL_KEYBOARD_ROWS = [ ], ] as const satisfies readonly (readonly TerminalVirtualKeyboardControl[])[]; +export const TERMINAL_VIRTUAL_KEYBOARD_WIDE_ROWS = [ + [ + { type: "key", button: TERMINAL_VIRTUAL_KEY_BUTTONS.esc }, + { type: "key", button: TERMINAL_VIRTUAL_KEY_BUTTONS.tab }, + { type: "modifier", modifier: "ctrl" }, + { type: "modifier", modifier: "alt" }, + { type: "modifier", modifier: "shift" }, + { type: "key", button: TERMINAL_VIRTUAL_KEY_BUTTONS.left }, + { type: "key", button: TERMINAL_VIRTUAL_KEY_BUTTONS.down }, + { type: "key", button: TERMINAL_VIRTUAL_KEY_BUTTONS.up }, + { type: "key", button: TERMINAL_VIRTUAL_KEY_BUTTONS.right }, + { type: "paste" }, + { type: "keyboardToggle" }, + { type: "key", button: TERMINAL_VIRTUAL_KEY_BUTTONS.enter }, + ], +] as const satisfies readonly (readonly TerminalVirtualKeyboardControl[])[]; + +export const TERMINAL_VIRTUAL_KEYBOARD_THREE_COLUMN_ROWS = [ + [ + { type: "key", button: TERMINAL_VIRTUAL_KEY_BUTTONS.esc }, + { type: "key", button: TERMINAL_VIRTUAL_KEY_BUTTONS.tab }, + { type: "modifier", modifier: "ctrl" }, + ], + [ + { type: "modifier", modifier: "alt" }, + { type: "modifier", modifier: "shift" }, + { type: "keyboardToggle" }, + ], + [ + { type: "paste" }, + { type: "key", button: TERMINAL_VIRTUAL_KEY_BUTTONS.up }, + { type: "key", button: TERMINAL_VIRTUAL_KEY_BUTTONS.enter }, + ], + [ + { type: "key", button: TERMINAL_VIRTUAL_KEY_BUTTONS.left }, + { type: "key", button: TERMINAL_VIRTUAL_KEY_BUTTONS.down }, + { type: "key", button: TERMINAL_VIRTUAL_KEY_BUTTONS.right }, + ], +] as const satisfies readonly (readonly TerminalVirtualKeyboardControl[])[]; + +const TERMINAL_KEY_BAR_SINGLE_ROW_MIN_WIDTH = 660; +const TERMINAL_KEY_BAR_THREE_COLUMN_MAX_WIDTH = 300; + +export function resolveTerminalVirtualKeyboardRows(input: { + isCompact: boolean; + availableWidth: number; +}) { + if (input.availableWidth > 0 && input.availableWidth < TERMINAL_KEY_BAR_THREE_COLUMN_MAX_WIDTH) { + return TERMINAL_VIRTUAL_KEYBOARD_THREE_COLUMN_ROWS; + } + return !input.isCompact && input.availableWidth >= TERMINAL_KEY_BAR_SINGLE_ROW_MIN_WIDTH + ? TERMINAL_VIRTUAL_KEYBOARD_WIDE_ROWS + : TERMINAL_VIRTUAL_KEYBOARD_ROWS; +} + +export function shouldShowTerminalVirtualKeyBar(input: { + isNative: boolean; + isCompact: boolean; +}): boolean { + return input.isNative || input.isCompact; +} + export function getTerminalVirtualKeyboardControlId( control: TerminalVirtualKeyboardControl, ): string { From e9e479c6bac08ed1b9d74310c53cbcbca70aa48e Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Wed, 30 Sep 2026 13:56:22 +0100 Subject: [PATCH 019/100] fix(pi): stream running subagent transcripts (#5755) Follow Pi child session files while subagents run for gotgenes, tintinweb, and nicobailon pi-subagents. gotgenes reports the child file through a runtime bridge that captures the parent's service at session start; nico async runs settle from status.json live and from the persisted subagent-notify notice on replay. Closes #5752 --- .../server/agent/providers/pi/agent.test.ts | 2 + .../src/server/agent/providers/pi/agent.ts | 16 ++- .../providers/pi/extensions/child-session.ts | 109 +++++++++----- .../agent/providers/pi/extensions/contract.ts | 23 ++- .../gotgenes-pi-subagents/index.test.ts | 90 ++++++++++++ .../extensions/gotgenes-pi-subagents/index.ts | 56 ++++++-- .../gotgenes-pi-subagents/runtime-bridge.ts | 33 +++++ .../agent/providers/pi/extensions/host.ts | 133 ++++++++++++++++-- .../providers/pi/extensions/index.test.ts | 50 +++++++ .../agent/providers/pi/extensions/index.ts | 4 + .../pi/extensions/pi-subagents/index.test.ts | 108 +++++++++++++- .../pi/extensions/pi-subagents/index.ts | 112 ++++++++++++++- .../tintinweb-pi-subagents/index.test.ts | 18 +++ .../tintinweb-pi-subagents/index.ts | 10 +- 14 files changed, 691 insertions(+), 73 deletions(-) create mode 100644 packages/server/src/server/agent/providers/pi/extensions/gotgenes-pi-subagents/runtime-bridge.ts diff --git a/packages/server/src/server/agent/providers/pi/agent.test.ts b/packages/server/src/server/agent/providers/pi/agent.test.ts index 969b221f02..0130574211 100644 --- a/packages/server/src/server/agent/providers/pi/agent.test.ts +++ b/packages/server/src/server/agent/providers/pi/agent.test.ts @@ -165,11 +165,13 @@ async function loadPaseoExtensionListeners( const extension = (await import(pathToFileURL(extensionPath).href)) as { default: (piApi: { on: (event: string, listener: PaseoExtensionListener) => void; + events: { on: () => void }; registerCommand: () => void; }) => void; }; extension.default({ on: (event, listener) => listeners.set(event, listener), + events: { on: () => undefined }, registerCommand: () => undefined, }); return listeners; diff --git a/packages/server/src/server/agent/providers/pi/agent.ts b/packages/server/src/server/agent/providers/pi/agent.ts index 5fd7b051ef..9a37c9adc7 100644 --- a/packages/server/src/server/agent/providers/pi/agent.ts +++ b/packages/server/src/server/agent/providers/pi/agent.ts @@ -65,7 +65,11 @@ import { } from "./history-mapper.js"; import { materializeProviderImage } from "../provider-image-output.js"; import { PiCliRuntime } from "./cli-runtime.js"; -import { createPiExtensionHost, type PiExtensionEventOutput } from "./extensions/index.js"; +import { + createPiExtensionHost, + piExtensionRuntimeBridge, + type PiExtensionEventOutput, +} from "./extensions/index.js"; import { revertPiConversation } from "./rewind.js"; import { listPiImportableSessions, readPiImportSessionConfig } from "./session-descriptor.js"; import type { PiRuntime, PiRuntimeSession, PiStartSessionInput } from "./runtime.js"; @@ -644,6 +648,7 @@ function createPiPaseoExtensionFile(systemPrompt?: string): PiTempFile { export default function paseoIntegration(pi) { const submittedUserMessages = []; + ${piExtensionRuntimeBridge} function emitSubmittedUserEntries(ctx) { const entries = ctx.sessionManager.getEntries(); @@ -1168,6 +1173,9 @@ export class PiRpcAgentSession implements AgentSession { this.extensionTimeoutMs = options.extensionTimeoutMs ?? DEFAULT_PI_EXTENSION_RESULT_TIMEOUT_MS; this.logger = options.logger; this.extensionHost = createPiExtensionHost(this.logger); + this.extensionHost.follow((event) => { + if (!this.closeController.signal.aborted) this.emit(event); + }); this.usagePoller = new PiUsagePoller({ scheduler: options.usagePollScheduler, readStats: () => this.runtimeSession.getSessionStats(), @@ -1523,6 +1531,7 @@ export class PiRpcAgentSession implements AgentSession { } this.closed = true; this.closeController.abort(); + this.extensionHost.close(); this.usagePoller.close(); try { await this.runtimeSession.close(); @@ -1936,6 +1945,11 @@ export class PiRpcAgentSession implements AgentSession { ): void { const message = optionalString(event.message); if (event.method === "notify" && message) { + const extensionMapping = this.extensionHost.mapRuntimeNotification(message); + if (extensionMapping) { + this.emitExtensionOutput(extensionMapping, this.currentTurnIdForEvent()); + return; + } if ( this.handleSubmittedUserEntryMarker(message) || this.handleEntryCaptureMarker(message) || diff --git a/packages/server/src/server/agent/providers/pi/extensions/child-session.ts b/packages/server/src/server/agent/providers/pi/extensions/child-session.ts index d2dfa0e99b..2013b831be 100644 --- a/packages/server/src/server/agent/providers/pi/extensions/child-session.ts +++ b/packages/server/src/server/agent/providers/pi/extensions/child-session.ts @@ -3,57 +3,96 @@ import { limitAgentTimelineItemContent } from "../../../agent-timeline-content.j import type { ProviderSubagentInputEvent } from "../../../provider-subagents/store.js"; import { PiHistoryMapper } from "../history-mapper.js"; import type { PiAgentMessage } from "../rpc-types.js"; +import { extractTextFromToolResult, type PiToolResult } from "../tool-call-mapper.js"; const MAX_BYTES = 2 * 1024 * 1024; const MAX_ITEMS = 200; -/** Child session files are immutable once the completed result exposes their path. */ -export async function mapPiChildSession( +export function outputFileFromToolResult(result: PiToolResult): string | undefined { + return extractTextFromToolResult(result)?.match(/^Output file:\s*(\S+)$/m)?.[1]; +} + +/** Reads a finished child transcript once, up to `maxBytes`. */ +export function mapPiChildSession( id: string, file: string, maxBytes = MAX_BYTES, ): Promise { + return new PiChildSessionFollower(id, file, maxBytes).readNew(); +} + +function mapChildLine( + mapper: PiHistoryMapper, + id: string, + line: string, +): ProviderSubagentInputEvent[] { + let entry: { message?: PiAgentMessage; timestamp?: string }; try { - const handle = await open(file, "r"); - try { - const fileSize = (await handle.stat()).size; - const size = Math.min(fileSize, MAX_BYTES, maxBytes); - if (!size) return []; - const buffer = Buffer.alloc(size); - const { bytesRead } = await handle.read(buffer, 0, size, 0); - const text = buffer.toString("utf8", 0, bytesRead); - const completeText = bytesRead < fileSize ? text.slice(0, text.lastIndexOf("\n")) : text; - return parseChildTimeline(id, completeText); - } finally { - await handle.close(); - } + entry = JSON.parse(line); } catch { return []; } + if (!entry.message || typeof entry.message !== "object" || !("role" in entry.message)) return []; + return mapper.mapMessages([entry.message]).flatMap((mapped) => + mapped.type === "timeline" + ? [ + { + type: "timeline" as const, + id, + item: limitAgentTimelineItemContent(mapped.item), + ...(entry.timestamp ? { timestamp: entry.timestamp } : {}), + }, + ] + : [], + ); } -function parseChildTimeline(id: string, text: string): ProviderSubagentInputEvent[] { - const mapper = new PiHistoryMapper("pi"); - const events: ProviderSubagentInputEvent[] = []; - for (const line of text.split("\n")) { - if (!line || events.length >= MAX_ITEMS) break; - let entry: { message?: PiAgentMessage; timestamp?: string }; +/** Incrementally reads one append-only child transcript. A partial JSONL row is held until newline. */ +export class PiChildSessionFollower { + private offset = 0; + private pending = Buffer.alloc(0); + private readonly mapper = new PiHistoryMapper("pi"); + private items = 0; + private readonly maxBytes: number; + + constructor( + private readonly id: string, + private readonly file: string, + maxBytes = MAX_BYTES, + ) { + this.maxBytes = Math.min(maxBytes, MAX_BYTES); + } + + async readNew(): Promise { + if (this.offset >= this.maxBytes || this.items >= MAX_ITEMS) return []; try { - entry = JSON.parse(line); + const handle = await open(this.file, "r"); + try { + const size = Math.min((await handle.stat()).size, this.maxBytes) - this.offset; + if (size <= 0) return []; + const buffer = Buffer.alloc(size); + const { bytesRead } = await handle.read(buffer, 0, size, this.offset); + this.offset += bytesRead; + const data = Buffer.concat([this.pending, buffer.subarray(0, bytesRead)]); + const lastNewline = data.lastIndexOf(10); + if (lastNewline < 0) { + this.pending = data; + return []; + } + this.pending = data.subarray(lastNewline + 1); + const events: ProviderSubagentInputEvent[] = []; + for (const line of data.subarray(0, lastNewline).toString("utf8").split("\n")) { + if (this.items >= MAX_ITEMS) break; + const mapped = mapChildLine(this.mapper, this.id, line).slice(0, MAX_ITEMS - this.items); + this.items += mapped.length; + events.push(...mapped); + } + return events; + } finally { + await handle.close(); + } } catch { - continue; - } - if (!entry.message || typeof entry.message !== "object" || !("role" in entry.message)) continue; - for (const mapped of mapper.mapMessages([entry.message])) { - if (mapped.type !== "timeline") continue; - events.push({ - type: "timeline", - id, - item: limitAgentTimelineItemContent(mapped.item), - ...(entry.timestamp ? { timestamp: entry.timestamp } : {}), - }); - if (events.length >= MAX_ITEMS) break; + return []; } } - return events; } diff --git a/packages/server/src/server/agent/providers/pi/extensions/contract.ts b/packages/server/src/server/agent/providers/pi/extensions/contract.ts index 7f9330eaeb..93129a0476 100644 --- a/packages/server/src/server/agent/providers/pi/extensions/contract.ts +++ b/packages/server/src/server/agent/providers/pi/extensions/contract.ts @@ -22,13 +22,21 @@ export interface PiExtensionToolMapping { detail?: ToolCallDetail; timeline?: AgentTimelineItem[]; subagents?: ProviderSubagentInputEvent[]; - /** Completed Pi child sessions to hydrate through the normal Pi history mapper. */ - childSessions?: Array<{ id: string; file: string }>; + childSessions?: PiChildSessionFile[]; } export interface PiExtensionCustomMapping { subagents: ProviderSubagentInputEvent[]; - childSessions?: Array<{ id: string; file: string }>; + childSessions?: PiChildSessionFile[]; +} + +/** + * A Pi child's append-only session file, mapped through the normal Pi history mapper. Report it as + * soon as it is known; a live session follows it until the child's upsert reaches a final status. + */ +export interface PiChildSessionFile { + id: string; + file: string; } export type PiExtensionDialog = Extract; @@ -48,6 +56,10 @@ export interface PiExtensionUiReply { export interface PiExtensionSession { mapToolCall?(call: PiExtensionToolCall): PiExtensionToolMapping | undefined; + /** Claims a `ctx.ui.notify` message sent by this extension's `runtimeBridge`. */ + mapRuntimeNotification?(message: string): PiExtensionCustomMapping | undefined; + /** Called on the live follow cadence to report state Pi only writes to disk. */ + poll?(): PiExtensionCustomMapping | undefined; mapCustomMessage?( message: Extract, ): PiExtensionCustomMapping | undefined; @@ -62,5 +74,10 @@ export interface PiExtensionSession { export interface PiExtension { id: string; + /** + * JavaScript statements run inside Paseo's Pi integration extension, with `pi` in scope. Reports + * state that Pi's RPC stream omits through `ctx.ui.notify`, which reaches `mapRuntimeNotification`. + */ + runtimeBridge?: string; createSession(): PiExtensionSession; } diff --git a/packages/server/src/server/agent/providers/pi/extensions/gotgenes-pi-subagents/index.test.ts b/packages/server/src/server/agent/providers/pi/extensions/gotgenes-pi-subagents/index.test.ts index 0e15438384..8e3d6e64e9 100644 --- a/packages/server/src/server/agent/providers/pi/extensions/gotgenes-pi-subagents/index.test.ts +++ b/packages/server/src/server/agent/providers/pi/extensions/gotgenes-pi-subagents/index.test.ts @@ -3,8 +3,98 @@ import { fileURLToPath } from "node:url"; import { createPiExtensionHost } from "../index.js"; import { readSubagentFixture, verifySubagentFixture } from "../subagent-fixture-test.js"; import { streamPiHistory } from "../../history-mapper.js"; +import { GOTGENES_CHILD_SESSION_MARKER, gotgenesRuntimeBridge } from "./runtime-bridge.js"; + +const SERVICE_KEY = Symbol.for("@gotgenes/pi-subagents:service"); + +function loadRuntimeBridge() { + const listeners = new Map void>(); + const pi = { + on: (name: string, listener: (event: unknown, ctx?: unknown) => void) => + listeners.set(name, listener), + events: { + on: (name: string, listener: (event: unknown) => void) => listeners.set(name, listener), + }, + }; + new Function("pi", gotgenesRuntimeBridge)(pi); + return (name: string, event: unknown, ctx?: unknown) => listeners.get(name)?.(event, ctx); +} + +function publishService(agents: Array<{ id: string; outputFile?: string }>): void { + (globalThis as Record)[SERVICE_KEY] = { listAgents: () => agents }; +} describe("@gotgenes/pi-subagents adapter", () => { + test("reports a child's file after the child republishes the gotgenes service", async () => { + const notifications: string[] = []; + const emit = loadRuntimeBridge(); + const file = "/sessions/parent/tasks/2026-09-30_child-session.jsonl"; + try { + publishService([{ id: "native-1", outputFile: file }]); + emit( + "session_start", + {}, + { ui: { notify: (message: string) => notifications.push(message) } }, + ); + // Pi loads gotgenes again inside the child, and that copy replaces the global service. + publishService([]); + emit("subagents:child:session-created", { sessionId: "child-session" }); + + await expect.poll(() => notifications).toHaveLength(1); + const host = createPiExtensionHost(); + host.mapToolCall({ + callId: "call-1", + toolName: "subagent", + args: { subagent_type: "Explore", prompt: "Inspect" }, + status: "completed", + result: { details: { agentId: "native-1", status: "background" } }, + }); + expect(host.mapRuntimeNotification(notifications[0]!)?.childSessions).toEqual([ + { id: "call-1", file }, + ]); + } finally { + delete (globalThis as Record)[SERVICE_KEY]; + } + }); + + test("accepts a live child path before or after the spawn result", () => { + for (const early of [true, false]) { + const host = createPiExtensionHost(); + const marker = `${GOTGENES_CHILD_SESSION_MARKER} ${JSON.stringify({ agentId: "native-1", file: "/tmp/child.jsonl" })}`; + if (early) expect(host.mapRuntimeNotification(marker)?.subagents).toEqual([]); + const spawn = host.mapToolCall({ + callId: "call-1", + toolName: "subagent", + args: { subagent_type: "Explore", prompt: "Inspect" }, + status: "completed", + result: { details: { agentId: "native-1", status: "background" } }, + }); + const path = early + ? spawn?.childSessions + : host.mapRuntimeNotification(marker)?.childSessions; + expect(path).toEqual([{ id: "call-1", file: "/tmp/child.jsonl" }]); + } + }); + + test("accepts a running subagent-update without a status field", () => { + const host = createPiExtensionHost(); + host.mapToolCall({ + callId: "call-1", + toolName: "subagent", + args: { subagent_type: "Explore", prompt: "Inspect" }, + status: "completed", + result: { details: { agentId: "native-1", status: "background" } }, + }); + const update = host.mapCustomMessage({ + role: "custom", + customType: "subagent-update", + content: "progress", + details: { id: "native-1", description: "Inspect", message: "progress" }, + }); + expect(update?.subagents).toEqual([ + { type: "upsert", id: "call-1", description: "Inspect", status: "running" }, + ]); + }); test("maps captured foreground lifecycle live and on replay", async () => { const events = await verifySubagentFixture( readSubagentFixture(new URL("./fixtures/foreground.json", import.meta.url)), diff --git a/packages/server/src/server/agent/providers/pi/extensions/gotgenes-pi-subagents/index.ts b/packages/server/src/server/agent/providers/pi/extensions/gotgenes-pi-subagents/index.ts index bf7d2813fb..62db93c79e 100644 --- a/packages/server/src/server/agent/providers/pi/extensions/gotgenes-pi-subagents/index.ts +++ b/packages/server/src/server/agent/providers/pi/extensions/gotgenes-pi-subagents/index.ts @@ -1,5 +1,7 @@ import { z } from "zod"; import { extractTextFromToolResult } from "../../tool-call-mapper.js"; +import { outputFileFromToolResult } from "../child-session.js"; +import { GOTGENES_CHILD_SESSION_MARKER, gotgenesRuntimeBridge } from "./runtime-bridge.js"; import type { PiExtension, PiExtensionToolCall } from "../contract.js"; const SpawnArgs = z @@ -22,11 +24,15 @@ const Details = z const Notification = z .object({ id: z.string().trim().min(1), - status: z.string(), + status: z.string().optional(), description: z.string().optional(), outputFile: z.string().trim().min(1).optional(), }) .passthrough(); +const LiveSession = z.object({ + agentId: z.string().trim().min(1), + file: z.string().trim().min(1), +}); const status = (value: string): "running" | "completed" | "failed" | "canceled" => { if (value === "completed") return "completed"; if (value === "error") return "failed"; @@ -36,9 +42,16 @@ const status = (value: string): "running" | "completed" | "failed" | "canceled" export const gotgenesPiSubagents: PiExtension = { id: "@gotgenes/pi-subagents", + runtimeBridge: gotgenesRuntimeBridge, createSession: () => { const callsByAgent = new Map(); const readSessions = new Set(); + const pendingFilesByAgent = new Map(); + const childSession = (id: string, file: string | undefined) => { + if (!file || readSessions.has(file)) return []; + readSessions.add(file); + return [{ id, file }]; + }; const mapSpawn = (call: PiExtensionToolCall) => { const args = SpawnArgs.safeParse(call.args); if (!args.success) return undefined; @@ -75,6 +88,10 @@ export const gotgenesPiSubagents: PiExtension = { } : { detail }; callsByAgent.set(details.data.agentId, call.callId); + const file = + outputFileFromToolResult(call.result) ?? pendingFilesByAgent.get(details.data.agentId); + pendingFilesByAgent.delete(details.data.agentId); + const childSessions = childSession(call.callId, file); return { detail, subagents: [ @@ -87,6 +104,7 @@ export const gotgenesPiSubagents: PiExtension = { status: status(details.data.status), }, ], + childSessions, }; }; const mapFollowup = (call: PiExtensionToolCall) => { @@ -104,11 +122,7 @@ export const gotgenesPiSubagents: PiExtension = { }; if (call.status === "running" || !details.success) return { detail }; const file = details.data.transcriptPath; - const childSessions = - file && status(details.data.status) !== "running" && !readSessions.has(file) - ? [{ id, file }] - : []; - if (childSessions.length && file) readSessions.add(file); + const childSessions = childSession(id, file); return { detail, subagents: [{ type: "upsert" as const, id, status: status(details.data.status) }], @@ -135,23 +149,41 @@ export const gotgenesPiSubagents: PiExtension = { const id = callsByAgent.get(details.data.id); if (!id) return undefined; const file = details.data.outputFile; - const childSessions = - file && status(details.data.status) !== "running" && !readSessions.has(file) - ? [{ id, file }] - : []; - if (childSessions.length && file) readSessions.add(file); + const childSessions = childSession(id, file); return { subagents: [ { type: "upsert", id, description: details.data.description, - status: status(details.data.status), + status: details.data.status ? status(details.data.status) : "running", }, ], childSessions, }; }, + mapRuntimeNotification(message) { + const prefix = `${GOTGENES_CHILD_SESSION_MARKER} `; + if (!message.startsWith(prefix)) return undefined; + let raw: unknown; + try { + raw = JSON.parse(message.slice(prefix.length)); + } catch { + return { subagents: [] }; + } + const parsed = LiveSession.safeParse(raw); + if (!parsed.success) return { subagents: [] }; + const { agentId, file } = parsed.data; + const id = callsByAgent.get(agentId); + if (!id) { + pendingFilesByAgent.set(agentId, file); + return { subagents: [] }; + } + return { + subagents: [{ type: "upsert", id, status: "running" }], + childSessions: childSession(id, file), + }; + }, }; }, }; diff --git a/packages/server/src/server/agent/providers/pi/extensions/gotgenes-pi-subagents/runtime-bridge.ts b/packages/server/src/server/agent/providers/pi/extensions/gotgenes-pi-subagents/runtime-bridge.ts new file mode 100644 index 0000000000..386fbb4705 --- /dev/null +++ b/packages/server/src/server/agent/providers/pi/extensions/gotgenes-pi-subagents/runtime-bridge.ts @@ -0,0 +1,33 @@ +export const GOTGENES_CHILD_SESSION_MARKER = "PASEO_GOTGENES_CHILD_SESSION"; + +/** + * Runs inside Pi. The spawn result omits the child's session file, so the parent's public service + * is the live source. Each child session loads its own copy of gotgenes, which republishes the + * global service and deletes it on exit, so the parent's service is captured at session_start. + */ +export const gotgenesRuntimeBridge = ` + let gotgenesParent; + pi.on("session_start", (_event, ctx) => { + gotgenesParent = { ctx, service: globalThis[Symbol.for("@gotgenes/pi-subagents:service")] }; + }); + pi.events.on("subagents:child:session-created", (event) => { + const parent = gotgenesParent; + let attempts = 0; + // The record gets its session file after the child's extensions bind. + const report = () => { + const record = parent?.service?.listAgents().find((agent) => + agent.outputFile?.includes(event.sessionId), + ); + if (record) { + parent.ctx.ui.notify( + "${GOTGENES_CHILD_SESSION_MARKER} " + + JSON.stringify({ agentId: record.id, file: record.outputFile }), + "info", + ); + } else if (++attempts < 240) { + setTimeout(report, 250); + } + }; + setTimeout(report, 0); + }); +`; diff --git a/packages/server/src/server/agent/providers/pi/extensions/host.ts b/packages/server/src/server/agent/providers/pi/extensions/host.ts index ae0f307393..8915e3df01 100644 --- a/packages/server/src/server/agent/providers/pi/extensions/host.ts +++ b/packages/server/src/server/agent/providers/pi/extensions/host.ts @@ -5,7 +5,7 @@ import type { AgentStreamEvent, } from "../../../agent-sdk-types.js"; import type { PiAgentMessage } from "../rpc-types.js"; -import { mapPiChildSession } from "./child-session.js"; +import { mapPiChildSession, PiChildSessionFollower } from "./child-session.js"; import type { PiExtension, PiExtensionCustomMapping, @@ -30,9 +30,19 @@ interface Session { adapter: PiExtensionSession; } +interface Follower { + reader: PiChildSessionFollower; + /** Reads run in order so a final read cannot overtake a tick. */ + pending: Promise; +} + export class PiExtensionHost { private readonly sessions: Session[] = []; private remainingHydrationBytes: number; + private readonly followers = new Map(); + private onFollowEvent?: (event: AgentStreamEvent) => void; + private followTimer?: ReturnType; + private pollingExtensions = false; constructor( extensions: readonly PiExtension[], @@ -47,6 +57,56 @@ export class PiExtensionHost { } } + /** Live sessions follow child files and adapter polls; replay hosts read each file once. */ + follow(onEvent: (event: AgentStreamEvent) => void): void { + this.onFollowEvent = onEvent; + this.followTimer ??= setInterval(() => { + for (const [id, follower] of this.followers) void this.readFollower(id, follower); + void this.pollExtensions(); + }, 250); + } + + close(): void { + if (this.followTimer) clearInterval(this.followTimer); + this.followTimer = undefined; + this.onFollowEvent = undefined; + this.followers.clear(); + } + + private readFollower(id: string, follower: Follower): Promise { + follower.pending = follower.pending + .then(async () => { + const events = await follower.reader.readNew(); + for (const event of events) { + this.onFollowEvent?.({ type: "provider_subagent", provider: "pi", event }); + } + return undefined; + }) + .catch((error) => { + this.logger?.warn( + { err: error, operation: "follow", childId: id }, + "Pi child follow failed", + ); + }); + return follower.pending; + } + + private async pollExtensions(): Promise { + if (this.pollingExtensions || !this.onFollowEvent) return; + this.pollingExtensions = true; + try { + for (const { id, adapter } of this.sessions) { + const mapping = this.safe(id, "poll", () => adapter.poll?.()); + if (!mapping) continue; + const output = this.prepare(id, mapping); + for (const event of output.events) this.onFollowEvent?.(event); + for (const event of await output.hydration) this.onFollowEvent?.(event); + } + } finally { + this.pollingExtensions = false; + } + } + private safe(id: string, operation: string, call: () => T): T | undefined { try { return call(); @@ -80,6 +140,19 @@ export class PiExtensionHost { return undefined; } + mapRuntimeNotification(message: string): PiExtensionOutput | undefined { + for (const { id, adapter } of this.sessions) { + const mapping = this.safe(id, "mapRuntimeNotification", () => + adapter.mapRuntimeNotification?.(message), + ); + if (mapping) { + const prepared = this.safe(id, "prepare", () => this.prepare(id, mapping)); + if (prepared) return prepared; + } + } + return undefined; + } + private prepare( id: string, mapping: T, @@ -92,25 +165,57 @@ export class PiExtensionHost { (event): AgentStreamEvent => ({ type: "provider_subagent", provider: "pi", event }), ), ]; - const hydration = Promise.all( - (mapping.childSessions ?? []).map(async ({ id: childId, file }) => { + const hydration = ( + this.onFollowEvent ? this.followChildren(mapping) : this.readChildren(mapping) + ).catch((error): AgentStreamEvent[] => { + this.logger?.warn( + { err: error, extensionId: id, operation: "hydrate" }, + "Pi extension adapter failed", + ); + return []; + }); + return { ...mapping, events, hydration }; + } + + private async followChildren( + mapping: PiExtensionToolMapping | PiExtensionCustomMapping, + ): Promise { + for (const { id, file } of mapping.childSessions ?? []) { + if (!this.followers.has(id)) { + this.followers.set(id, { + reader: new PiChildSessionFollower(id, file), + pending: Promise.resolve(), + }); + } + } + const finished = (mapping.subagents ?? []).flatMap((event) => + event.type === "upsert" && event.status && event.status !== "running" ? [event.id] : [], + ); + const reads = [ + ...new Set([...(mapping.childSessions ?? []).map(({ id }) => id), ...finished]), + ].flatMap((id) => { + const follower = this.followers.get(id); + return follower ? [this.readFollower(id, follower)] : []; + }); + for (const id of finished) this.followers.delete(id); + await Promise.all(reads); + return []; + } + + private async readChildren( + mapping: PiExtensionToolMapping | PiExtensionCustomMapping, + ): Promise { + const groups = await Promise.all( + (mapping.childSessions ?? []).map(async ({ id, file }) => { const bytes = Math.min(this.remainingHydrationBytes, 2 * 1024 * 1024); if (bytes <= 0) return []; this.remainingHydrationBytes -= bytes; - return (await this.readChildSession(childId, file, bytes)).map( + return (await this.readChildSession(id, file, bytes)).map( (event): AgentStreamEvent => ({ type: "provider_subagent", provider: "pi", event }), ); }), - ) - .then((groups) => groups.flat()) - .catch((error): AgentStreamEvent[] => { - this.logger?.warn( - { err: error, extensionId: id, operation: "hydrate" }, - "Pi extension adapter failed", - ); - return []; - }); - return { ...mapping, events, hydration }; + ); + return groups.flat(); } onToolStart(call: PiExtensionToolCall, provider = "pi"): AgentPermissionRequest | undefined { diff --git a/packages/server/src/server/agent/providers/pi/extensions/index.test.ts b/packages/server/src/server/agent/providers/pi/extensions/index.test.ts index dd5d76601a..9afacae977 100644 --- a/packages/server/src/server/agent/providers/pi/extensions/index.test.ts +++ b/packages/server/src/server/agent/providers/pi/extensions/index.test.ts @@ -1,4 +1,7 @@ import { describe, expect, test } from "vitest"; +import { mkdtemp, appendFile, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; import { createPiExtensionHost } from "./index.js"; import { PiHistoryMapper } from "../history-mapper.js"; import { parseToolArgs, parseToolResult } from "../tool-call-mapper.js"; @@ -30,6 +33,53 @@ const throwingAdapter: PiExtension = { }; describe("Pi extension host", () => { + test("streams appended child rows before the child completes", async () => { + const dir = await mkdtemp(join(tmpdir(), "paseo-pi-live-child-")); + const file = join(dir, "child.jsonl"); + const received: string[] = []; + const host = new PiExtensionHost([ + { + id: "child", + createSession: () => ({ + mapToolCall: () => ({ + subagents: [{ type: "upsert", id: "child-1", status: "running" }], + childSessions: [{ id: "child-1", file }], + }), + }), + }, + ]); + try { + host.follow((event) => { + if (event.type === "provider_subagent" && event.event.type === "timeline") { + received.push(JSON.stringify(event.event.item)); + } + }); + host.mapToolCall({ + callId: "x", + toolName: "child", + args: {}, + status: "completed", + result: null, + }); + await appendFile( + file, + '{"type":"message","message":{"role":"assistant","content":[{"type":"text","text":"BEGIN"}]}}\n', + ); + await expect.poll(() => received.length).toBe(1); + await appendFile( + file, + '{"type":"message","message":{"role":"assistant","content":[{"type":"text","text":"DO', + ); + await new Promise((resolve) => setTimeout(resolve, 350)); + expect(received).toHaveLength(1); + await appendFile(file, 'NE"}]}}\n'); + await expect.poll(() => received.length).toBe(2); + expect(received.join(" ")).toContain("DONE"); + } finally { + host.close(); + await rm(dir, { recursive: true, force: true }); + } + }); test("declines a throwing adapter and preserves generic history", async () => { const host = createPiExtensionHost(undefined, [throwingAdapter]); const mapper = new PiHistoryMapper("pi", [], {}, host); diff --git a/packages/server/src/server/agent/providers/pi/extensions/index.ts b/packages/server/src/server/agent/providers/pi/extensions/index.ts index eb4ecda059..2ac68178da 100644 --- a/packages/server/src/server/agent/providers/pi/extensions/index.ts +++ b/packages/server/src/server/agent/providers/pi/extensions/index.ts @@ -3,6 +3,10 @@ import { piExtensions } from "./registry.js"; import type { Logger } from "pino"; import type { PiExtension } from "./contract.js"; +export const piExtensionRuntimeBridge = piExtensions + .flatMap((extension) => extension.runtimeBridge ?? []) + .join("\n"); + export type { PiExtensionHost } from "./host.js"; export type { PiExtensionEventOutput } from "./host.js"; diff --git a/packages/server/src/server/agent/providers/pi/extensions/pi-subagents/index.test.ts b/packages/server/src/server/agent/providers/pi/extensions/pi-subagents/index.test.ts index 2f3293f8c1..8e9db54047 100644 --- a/packages/server/src/server/agent/providers/pi/extensions/pi-subagents/index.test.ts +++ b/packages/server/src/server/agent/providers/pi/extensions/pi-subagents/index.test.ts @@ -1,5 +1,8 @@ import { describe, expect, test } from "vitest"; import { fileURLToPath } from "node:url"; +import { mkdtemp, writeFile, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; import { createPiExtensionHost } from "../index.js"; import { parseToolArgs, @@ -27,7 +30,100 @@ function mapToolDetail(toolCall: PiTrackedToolCall, result: PiToolResult) { return mapping(toolCall, result)?.detail; } +function hasChildTimeline(events: AgentStreamEvent[]) { + return events.some( + (event) => event.type === "provider_subagent" && event.event.type === "timeline", + ); +} + +function hasCompletedChild(events: AgentStreamEvent[]) { + return events.some( + (event) => + event.type === "provider_subagent" && + event.event.type === "upsert" && + event.event.status === "completed", + ); +} + describe("pi-subagents adapter", () => { + test("follows async status and transcript without bg_wait", async () => { + const dir = await mkdtemp(join(tmpdir(), "paseo-pi-async-")); + const file = join(dir, "child.jsonl"); + const received: AgentStreamEvent[] = []; + const host = createPiExtensionHost(); + try { + host.follow((event) => received.push(event)); + host.mapToolCall({ + callId: "call-1", + toolName: "subagent", + args: { agent: "scout", task: "Inspect", async: true }, + status: "completed", + result: { + details: { mode: "single", runId: "run-1", asyncId: "run-1", asyncDir: dir, results: [] }, + }, + }); + await writeFile( + file, + '{"type":"message","message":{"role":"assistant","content":[{"type":"text","text":"BEGIN"}]}}\n', + ); + await writeFile( + join(dir, "status.json"), + JSON.stringify({ + state: "running", + steps: [{ agent: "scout", status: "running", sessionFile: file }], + }), + ); + await expect.poll(() => hasChildTimeline(received)).toBe(true); + await writeFile( + join(dir, "status.json"), + JSON.stringify({ + state: "complete", + steps: [{ agent: "scout", status: "complete", sessionFile: file }], + }), + ); + await expect.poll(() => hasCompletedChild(received)).toBe(true); + } finally { + host.close(); + await rm(dir, { recursive: true, force: true }); + } + }); + test("settles an async run and reads its transcript from the completion notice on replay", async () => { + const dir = await mkdtemp(join(tmpdir(), "paseo-pi-async-")); + const file = join(dir, "child.jsonl"); + try { + await writeFile( + file, + '{"type":"message","message":{"role":"assistant","content":[{"type":"text","text":"DONE"}]}}\n', + ); + const host = createPiExtensionHost(); + host.mapToolCall({ + callId: "call-1", + toolName: "subagent", + args: { agent: "scout", task: "Inspect", async: true }, + status: "completed", + result: { + details: { mode: "single", runId: "run-1", asyncId: "run-1", asyncDir: dir, results: [] }, + }, + }); + const notice = host.mapCustomMessage({ + role: "custom", + customType: "subagent-notify", + content: [ + "Background task completed: **scout**", + "", + "Session file: /tmp/not-the-session-line.jsonl", + "", + `Retention-managed async directory: ${dir}`, + "", + `Session file: ${file}`, + ].join("\n"), + }); + expect(notice?.subagents).toEqual([{ type: "upsert", id: "call-1", status: "completed" }]); + expect(hasChildTimeline(await notice!.hydration)).toBe(true); + } finally { + await rm(dir, { recursive: true, force: true }); + } + }); test("maps completed subagent calls with task input to sub-agent detail", () => { const toolCall = parseToolArgs("subagent", { agent: "reviewer", @@ -74,11 +170,11 @@ describe("pi-subagents adapter", () => { expect(events.filter((event) => event.event.type === "timeline").length).toBeGreaterThan(0); }); - test("keeps captured async runs running until structured completion", async () => { + test("settles captured async runs from the completion notice live and on replay", async () => { const fixture = readSubagentFixture(new URL("./fixtures/background.json", import.meta.url)); const events = await verifySubagentFixture(fixture); expect(events.findLast((event) => event.event.type === "upsert")?.event).toEqual( - expect.objectContaining({ status: "running" }), + expect.objectContaining({ status: "completed" }), ); }); @@ -102,7 +198,13 @@ describe("pi-subagents adapter", () => { const upserts = events .filter((event) => event.event.type === "upsert") .map((event) => (event.event.type === "upsert" ? event.event : null)); - expect(upserts.map((event) => event?.status)).toEqual(["running", "running", "completed"]); + // bg_wait settles the child; the later completion notice repeats it. + expect(upserts.map((event) => event?.status)).toEqual([ + "running", + "running", + "completed", + "completed", + ]); expect(new Set(upserts.map((event) => event?.id)).size).toBe(1); expect(events.filter((event) => event.event.type === "timeline").length).toBeGreaterThan(0); }); diff --git a/packages/server/src/server/agent/providers/pi/extensions/pi-subagents/index.ts b/packages/server/src/server/agent/providers/pi/extensions/pi-subagents/index.ts index 0ec0f0ab47..64e814af3f 100644 --- a/packages/server/src/server/agent/providers/pi/extensions/pi-subagents/index.ts +++ b/packages/server/src/server/agent/providers/pi/extensions/pi-subagents/index.ts @@ -1,4 +1,6 @@ import { z } from "zod"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; import { extractTextFromToolResult } from "../../tool-call-mapper.js"; import type { PiExtension, PiExtensionToolCall, PiExtensionToolMapping } from "../contract.js"; @@ -27,16 +29,55 @@ const Details = z mode: z.string(), runId: z.string().optional(), asyncId: z.string().optional(), + asyncDir: z.string().trim().min(1).optional(), results: z.array(Row), completions: z.array(Completion).optional(), }) .passthrough(); +const AsyncStatus = z + .object({ + state: z.string(), + steps: z.array( + z + .object({ + agent: z.string().trim().min(1), + status: z.string().optional(), + sessionFile: z.string().trim().min(1).optional(), + }) + .passthrough(), + ), + }) + .passthrough(); + +function asyncChildStatus(runState: string, stepStatus?: string) { + if (!["complete", "failed", "stopped"].includes(runState)) return "running"; + if (runState === "stopped" || stepStatus === "stopped") return "canceled"; + if (runState === "failed" || stepStatus === "failed" || stepStatus === "error") return "failed"; + return "completed"; +} + +/** The `subagent-notify` header; a grouped notice reports every run as completed. */ +function noticeStatus(header: string) { + if (/^Background tasks completed \(\d+\):/.test(header)) return "completed"; + const outcome = header.match( + /^(?:Background task|Detached foreground task) (completed|failed|stopped): \*\*/, + )?.[1]; + if (outcome === "stopped") return "canceled"; + return outcome as "completed" | "failed" | undefined; +} export const piSubagents: PiExtension = { id: "pi-subagents", createSession: () => { const callsByRun = new Map(); const readSessions = new Set(); + /** Keyed by async directory, which both status.json and the completion notice carry. */ + const asyncRuns = new Map(); + const rememberAsyncRun = (details: z.infer, call: PiExtensionToolCall) => { + if (details.asyncDir && call.status !== "failed") { + asyncRuns.set(details.asyncDir, { owner: call.callId, polling: true }); + } + }; const collectRows = ( owner: string, rows: z.infer[], @@ -109,7 +150,8 @@ export const piSubagents: PiExtension = { ? { detail, subagents: [{ ...base, status: "failed" as const }] } : { detail }; if (details.data.runId) callsByRun.set(details.data.runId, call.callId); - if (details.data.results.length === 0 && (details.data.asyncId || args.data.async)) + if (details.data.results.length === 0 && (details.data.asyncId || args.data.async)) { + rememberAsyncRun(details.data, call); return { detail, subagents: [ @@ -119,6 +161,7 @@ export const piSubagents: PiExtension = { }, ], }; + } if (details.data.results.length === 0 && call.status === "failed") return { detail, subagents: [{ ...base, status: "failed" as const }] }; return { @@ -132,6 +175,73 @@ export const piSubagents: PiExtension = { if (call.toolName === "subagent") return mapSpawn(call); return undefined; }, + // The notice is the only completion Pi persists, so replay settles async runs from it. + mapCustomMessage(message) { + if (message.customType !== "subagent-notify") return undefined; + const text = + typeof message.content === "string" + ? message.content + : extractTextFromToolResult({ content: message.content }); + const lines = text?.split("\n") ?? []; + const status = noticeStatus(lines[0] ?? ""); + if (!status) return undefined; + const subagents: NonNullable = []; + const childSessions: NonNullable = []; + // Each run's session line follows its async directory line; earlier lines are child output. + let owner: string | undefined; + for (const line of lines) { + const dir = line.match(/^Retention-managed async directory: (.+)$/)?.[1]; + if (dir) { + owner = asyncRuns.get(dir)?.owner; + if (owner) subagents.push({ type: "upsert", id: owner, status }); + continue; + } + const file = owner ? line.match(/^Session file: (.+)$/)?.[1] : undefined; + if (!owner || !file) continue; + if (!readSessions.has(file)) { + readSessions.add(file); + childSessions.push({ id: owner, file }); + } + owner = undefined; + } + return subagents.length ? { subagents, childSessions } : undefined; + }, + poll() { + const subagents: NonNullable = []; + const childSessions: NonNullable = []; + for (const [dir, run] of asyncRuns) { + if (!run.polling) continue; + let raw: unknown; + try { + raw = JSON.parse(readFileSync(join(dir, "status.json"), "utf8")); + } catch { + continue; + } + const parsed = AsyncStatus.safeParse(raw); + if (!parsed.success) continue; + const terminal = ["complete", "failed", "stopped"].includes(parsed.data.state); + for (const [index, step] of parsed.data.steps.entries()) { + const id = parsed.data.steps.length === 1 ? run.owner : `${run.owner}:${index}`; + const status = asyncChildStatus(parsed.data.state, step.status); + subagents.push({ type: "upsert", id, title: step.agent, status }); + if (step.sessionFile && !readSessions.has(step.sessionFile)) { + readSessions.add(step.sessionFile); + childSessions.push({ id, file: step.sessionFile }); + } + } + if (terminal) { + if (parsed.data.steps.length === 0) { + subagents.push({ + type: "upsert", + id: run.owner, + status: parsed.data.state === "complete" ? "completed" : "failed", + }); + } + run.polling = false; + } + } + return subagents.length || childSessions.length ? { subagents, childSessions } : undefined; + }, }; }, }; diff --git a/packages/server/src/server/agent/providers/pi/extensions/tintinweb-pi-subagents/index.test.ts b/packages/server/src/server/agent/providers/pi/extensions/tintinweb-pi-subagents/index.test.ts index 62a8e5ea26..b464026ab1 100644 --- a/packages/server/src/server/agent/providers/pi/extensions/tintinweb-pi-subagents/index.test.ts +++ b/packages/server/src/server/agent/providers/pi/extensions/tintinweb-pi-subagents/index.test.ts @@ -4,6 +4,24 @@ import { createPiExtensionHost } from "../index.js"; import { readSubagentFixture, verifySubagentFixture } from "../subagent-fixture-test.js"; describe("@tintinweb/pi-subagents adapter", () => { + test("exposes the background output file while the child is running", () => { + const mapping = createPiExtensionHost().mapToolCall({ + callId: "call-1", + toolName: "Agent", + args: { subagent_type: "Explore", prompt: "Inspect" }, + status: "completed", + result: { + details: { agentId: "native-1", status: "background" }, + content: [ + { type: "text", text: "Agent started in background.\nOutput file: /tmp/child.output\n" }, + ], + }, + }); + expect(mapping?.subagents).toEqual([ + expect.objectContaining({ id: "call-1", status: "running" }), + ]); + expect(mapping?.childSessions).toEqual([{ id: "call-1", file: "/tmp/child.output" }]); + }); test("maps captured foreground lifecycle live and on replay", async () => { const events = await verifySubagentFixture( readSubagentFixture(new URL("./fixtures/foreground.json", import.meta.url)), diff --git a/packages/server/src/server/agent/providers/pi/extensions/tintinweb-pi-subagents/index.ts b/packages/server/src/server/agent/providers/pi/extensions/tintinweb-pi-subagents/index.ts index dcfe093328..2b06779afa 100644 --- a/packages/server/src/server/agent/providers/pi/extensions/tintinweb-pi-subagents/index.ts +++ b/packages/server/src/server/agent/providers/pi/extensions/tintinweb-pi-subagents/index.ts @@ -1,5 +1,6 @@ import { z } from "zod"; import { extractTextFromToolResult } from "../../tool-call-mapper.js"; +import { outputFileFromToolResult } from "../child-session.js"; import type { PiExtension, PiExtensionToolCall } from "../contract.js"; const SpawnArgs = z @@ -74,6 +75,9 @@ export const tintinwebPiSubagents: PiExtension = { } : { detail }; callsByAgent.set(details.data.agentId, call.callId); + const file = outputFileFromToolResult(call.result); + const childSessions = file && !readSessions.has(file) ? [{ id: call.callId, file }] : []; + if (file) readSessions.add(file); return { detail, subagents: [ @@ -86,6 +90,7 @@ export const tintinwebPiSubagents: PiExtension = { status: status(details.data.status), }, ], + childSessions, }; }; const mapFollowup = (call: PiExtensionToolCall) => { @@ -125,10 +130,7 @@ export const tintinwebPiSubagents: PiExtension = { const id = callsByAgent.get(details.data.id); if (!id) return undefined; const file = details.data.outputFile; - const childSessions = - file && status(details.data.status) !== "running" && !readSessions.has(file) - ? [{ id, file }] - : []; + const childSessions = file && !readSessions.has(file) ? [{ id, file }] : []; if (childSessions.length && file) readSessions.add(file); return { subagents: [ From 9952c55983c6698aec88e81b62c4bb267e3122e0 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Wed, 30 Sep 2026 13:56:24 +0100 Subject: [PATCH 020/100] fix(app): restore native tablet touch actions (#5713) Wide native tooltips followed the desktop hover policy, and native tab chips disabled their long-press menu. Enable the existing touch actions while keeping tab navigation on completed press so horizontal scrolling does not select a tab. Keep URL-opening links from leaving a tooltip over the app on return. Co-authored-by: Jichen --- .../native-wide-agent-tab-menu.android.ad | 12 ++++++ .../native-wide-agent-tab-menu.ios.ad | 14 +++++++ .../native-wide-context-meter.android.ad | 17 ++++++++ .../native-wide-context-meter.ios.ad | 17 ++++++++ .../native-wide-menu-preconditions.md | 33 +++++++++++++++ ...native-wide-overlay-dismissal.android.yaml | 40 +++++++++++++++++++ .../native-wide-overlay-dismissal.ios.yaml | 40 +++++++++++++++++++ .../native-wide-sidebar-rename.android.ad | 13 ++++++ .../native-wide-sidebar-rename.ios.ad | 16 ++++++++ .../native-wide-tab-scroll.android.ad | 17 ++++++++ .../native-wide-tab-scroll.ios.ad | 16 ++++++++ ...native-wide-tooltip-dismissal.android.yaml | 23 +++++++++++ .../native-wide-tooltip-dismissal.ios.yaml | 30 ++++++++++++++ .../src/components/context-window-meter.tsx | 2 +- .../app/src/components/ui/external-link.tsx | 4 +- packages/app/src/components/ui/tooltip.tsx | 11 ++--- .../workspace/workspace-desktop-tabs-row.tsx | 3 +- 17 files changed, 298 insertions(+), 10 deletions(-) create mode 100644 packages/app/e2e/mobile/agent-device/native-wide-agent-tab-menu.android.ad create mode 100644 packages/app/e2e/mobile/agent-device/native-wide-agent-tab-menu.ios.ad create mode 100644 packages/app/e2e/mobile/agent-device/native-wide-context-meter.android.ad create mode 100644 packages/app/e2e/mobile/agent-device/native-wide-context-meter.ios.ad create mode 100644 packages/app/e2e/mobile/agent-device/native-wide-menu-preconditions.md create mode 100644 packages/app/e2e/mobile/agent-device/native-wide-overlay-dismissal.android.yaml create mode 100644 packages/app/e2e/mobile/agent-device/native-wide-overlay-dismissal.ios.yaml create mode 100644 packages/app/e2e/mobile/agent-device/native-wide-sidebar-rename.android.ad create mode 100644 packages/app/e2e/mobile/agent-device/native-wide-sidebar-rename.ios.ad create mode 100644 packages/app/e2e/mobile/agent-device/native-wide-tab-scroll.android.ad create mode 100644 packages/app/e2e/mobile/agent-device/native-wide-tab-scroll.ios.ad create mode 100644 packages/app/e2e/mobile/agent-device/native-wide-tooltip-dismissal.android.yaml create mode 100644 packages/app/e2e/mobile/agent-device/native-wide-tooltip-dismissal.ios.yaml diff --git a/packages/app/e2e/mobile/agent-device/native-wide-agent-tab-menu.android.ad b/packages/app/e2e/mobile/agent-device/native-wide-agent-tab-menu.android.ad new file mode 100644 index 0000000000..6a53573100 --- /dev/null +++ b/packages/app/e2e/mobile/agent-device/native-wide-agent-tab-menu.android.ad @@ -0,0 +1,12 @@ +context platform=android timeout=60000 retries=0 +env APP_ID=sh.paseo.debug + +open "${APP_ID}" +wait "id=\"workspace-tabs-scroll\"" 20000 +wait "id=\"workspace-tab-agent_${AGENT_ID}\"" 20000 +longpress "id=\"workspace-tab-agent_${AGENT_ID}\"" 900 +wait "id=\"workspace-tab-context-${TAB_ID}\"" 10000 +is visible "id=\"workspace-tab-context-${TAB_ID}-copy-agent-id\"" +press "id=\"workspace-tab-context-${TAB_ID}-copy-agent-id\"" +wait "id=\"workspace-tabs-scroll\"" 10000 +close diff --git a/packages/app/e2e/mobile/agent-device/native-wide-agent-tab-menu.ios.ad b/packages/app/e2e/mobile/agent-device/native-wide-agent-tab-menu.ios.ad new file mode 100644 index 0000000000..fccfce679a --- /dev/null +++ b/packages/app/e2e/mobile/agent-device/native-wide-agent-tab-menu.ios.ad @@ -0,0 +1,14 @@ +context platform=ios timeout=60000 retries=0 +env APP_ID=sh.paseo.debug + +open "${APP_ID}" +wait "id=\"workspace-tabs-scroll\"" 20000 +wait "id=\"workspace-tab-agent_${AGENT_ID}\"" 20000 +longpress "id=\"workspace-tab-agent_${AGENT_ID}\"" 900 +wait "id=\"workspace-tab-context-${TAB_ID}\"" 10000 +is visible "id=\"workspace-tab-context-${TAB_ID}-copy-agent-id\"" +press "id=\"workspace-tab-context-${TAB_ID}-copy-agent-id\"" +wait "id=\"workspace-tab-agent_${OTHER_AGENT_ID}\"" 10000 +press "id=\"workspace-tab-agent_${OTHER_AGENT_ID}\"" +wait "id=\"agent-chat-scroll\"" 10000 +close diff --git a/packages/app/e2e/mobile/agent-device/native-wide-context-meter.android.ad b/packages/app/e2e/mobile/agent-device/native-wide-context-meter.android.ad new file mode 100644 index 0000000000..1c5de2da8b --- /dev/null +++ b/packages/app/e2e/mobile/agent-device/native-wide-context-meter.android.ad @@ -0,0 +1,17 @@ +context platform=android timeout=60000 retries=0 +env APP_ID=sh.paseo.debug + +open "${APP_ID}" +wait "id=\"workspace-tabs-scroll\"" 20000 +wait "id=\"context-window-meter\"" 20000 +press "id=\"context-window-meter\"" +wait "id=\"context-window-meter-tooltip\"" 10000 +press "id=\"tooltip-dismiss\"" +wait "id=\"context-window-meter\"" 10000 +press "id=\"context-window-meter\"" +wait "id=\"context-window-meter-tooltip\"" 10000 +press "id=\"tooltip-dismiss\"" +wait "id=\"workspace-tab-agent_${OTHER_AGENT_ID}\"" 10000 +press "id=\"workspace-tab-agent_${OTHER_AGENT_ID}\"" +wait "id=\"agent-chat-scroll\"" 10000 +close diff --git a/packages/app/e2e/mobile/agent-device/native-wide-context-meter.ios.ad b/packages/app/e2e/mobile/agent-device/native-wide-context-meter.ios.ad new file mode 100644 index 0000000000..3561b8b150 --- /dev/null +++ b/packages/app/e2e/mobile/agent-device/native-wide-context-meter.ios.ad @@ -0,0 +1,17 @@ +context platform=ios timeout=60000 retries=0 +env APP_ID=sh.paseo.debug + +open "${APP_ID}" +wait "id=\"workspace-tabs-scroll\"" 20000 +wait "id=\"context-window-meter\"" 20000 +press "id=\"context-window-meter\"" +wait "id=\"context-window-meter-tooltip\"" 10000 +press "id=\"tooltip-dismiss\"" +wait "id=\"context-window-meter\"" 10000 +press "id=\"context-window-meter\"" +wait "id=\"context-window-meter-tooltip\"" 10000 +press "id=\"tooltip-dismiss\"" +wait "id=\"workspace-tab-agent_${OTHER_AGENT_ID}\"" 10000 +press "id=\"workspace-tab-agent_${OTHER_AGENT_ID}\"" +wait "id=\"agent-chat-scroll\"" 10000 +close diff --git a/packages/app/e2e/mobile/agent-device/native-wide-menu-preconditions.md b/packages/app/e2e/mobile/agent-device/native-wide-menu-preconditions.md new file mode 100644 index 0000000000..cfb5c5f244 --- /dev/null +++ b/packages/app/e2e/mobile/agent-device/native-wide-menu-preconditions.md @@ -0,0 +1,33 @@ +# Native wide menu flows + +Run these on a connected task-owned tablet with the current development app and Metro bundle. `workspace-tabs-scroll` asserts the wide tab row; also pass an explicit tablet `--device` so a landscape phone above the breakpoint cannot stand in for tablet coverage. The context meter flow requires an agent tab with reported token usage selected. Its tooltip assertion uses a stable ID emitted only by the open content. + +The agent tab menu flow requires an existing agent tab. Set `AGENT_ID` to the ID of that agent and `TAB_ID` to its workspace tab ID (shown in the `workspace-tab-context-*` test ID) before replay. The flow long-presses the tab and selects **Copy agent id**. It does not claim that an arrow tap works. + +The sidebar Rename flow requires a selected workspace and its `WORKSPACE_KEY` (shown in the `sidebar-workspace-kebab-*` test ID). It opens the row's actions, opens Rename, then cancels without changing the workspace name. This covers the currently working row reported in #5226. + +For the native-wide tooltip dismissal flows, select an agent with reported token usage so `context-window-meter` exists. A fresh load-test stream can supply usage. The Maestro flows assert that the tooltip is absent after dismissal, then reopen it to prove the control is usable. The overlay dismissal flows assert that the tab menu and Rename dialog are absent after selection/cancel. On iPad the focused Rename input can consume the first Cancel tap; the iOS Maestro flow conditionally repeats that tap only while the dialog remains visible. + +For tab scrolling, first create enough tabs for actual horizontal overflow. The Android replay was validated with nine tabs and the Explorer sidebar open; the iPad replay was validated with six tabs. Set `DRAFT_ID` to a visible draft tab, `AGENT_ID` to the first visible agent tab, `OTHER_AGENT_ID` to another agent tab, and `TAB_ID` to the first agent's workspace tab ID. The short pan starts on an agent chip while a draft is selected; the draft composer must remain active. The flow then checks a stationary long press and an ordinary tap. Keep the tab row scrolled to its beginning before running the iPad flow. + +Android Pixel Tablet API 36.1: + +```sh +agent-device replay packages/app/e2e/mobile/agent-device/native-wide-context-meter.android.ad --platform android --device paseo-tablet-p1-api36 --env OTHER_AGENT_ID= +agent-device replay packages/app/e2e/mobile/agent-device/native-wide-agent-tab-menu.android.ad --platform android --device paseo-tablet-p1-api36 --env AGENT_ID= --env TAB_ID= +agent-device replay packages/app/e2e/mobile/agent-device/native-wide-sidebar-rename.android.ad --platform android --device paseo-tablet-p1-api36 --env WORKSPACE_KEY= +agent-device replay packages/app/e2e/mobile/agent-device/native-wide-tooltip-dismissal.android.yaml --maestro --platform android --device paseo-tablet-p1-api36 +agent-device replay packages/app/e2e/mobile/agent-device/native-wide-overlay-dismissal.android.yaml --maestro --platform android --device paseo-tablet-p1-api36 --env AGENT_ID= --env TAB_ID= --env WORKSPACE_KEY= +agent-device replay packages/app/e2e/mobile/agent-device/native-wide-tab-scroll.android.ad --platform android --device paseo-tablet-p1-api36 --env DRAFT_ID= --env AGENT_ID= --env OTHER_AGENT_ID= --env TAB_ID= +``` + +iPad Pro 11-inch (M5), iOS 26.5, from the Namespace Mac checkout: + +```sh +agent-device replay packages/app/e2e/mobile/agent-device/native-wide-context-meter.ios.ad --platform ios --device 'iPad Pro 11-inch (M5)' --env OTHER_AGENT_ID= +agent-device replay packages/app/e2e/mobile/agent-device/native-wide-agent-tab-menu.ios.ad --platform ios --device 'iPad Pro 11-inch (M5)' --env AGENT_ID= --env OTHER_AGENT_ID= --env TAB_ID= +agent-device replay packages/app/e2e/mobile/agent-device/native-wide-sidebar-rename.ios.ad --platform ios --device 'iPad Pro 11-inch (M5)' --env WORKSPACE_KEY= +agent-device replay packages/app/e2e/mobile/agent-device/native-wide-tooltip-dismissal.ios.yaml --maestro --platform ios --device 'iPad Pro 11-inch (M5)' +agent-device replay packages/app/e2e/mobile/agent-device/native-wide-overlay-dismissal.ios.yaml --maestro --platform ios --device 'iPad Pro 11-inch (M5)' --env WORKSPACE_KEY= +agent-device replay packages/app/e2e/mobile/agent-device/native-wide-tab-scroll.ios.ad --platform ios --device 'iPad Pro 11-inch (M5)' --env DRAFT_ID= --env AGENT_ID= --env OTHER_AGENT_ID= --env TAB_ID= +``` diff --git a/packages/app/e2e/mobile/agent-device/native-wide-overlay-dismissal.android.yaml b/packages/app/e2e/mobile/agent-device/native-wide-overlay-dismissal.android.yaml new file mode 100644 index 0000000000..39b11fd1af --- /dev/null +++ b/packages/app/e2e/mobile/agent-device/native-wide-overlay-dismissal.android.yaml @@ -0,0 +1,40 @@ +appId: sh.paseo.debug +--- +- launchApp +- extendedWaitUntil: + visible: + id: workspace-tabs-scroll + timeout: 30000 +- tapOn: + id: workspace-tab-agent_${AGENT_ID} +- longPressOn: + id: workspace-tab-agent_${AGENT_ID} +- assertVisible: + id: workspace-tab-context-${TAB_ID}-copy-agent-id +- tapOn: + id: workspace-tab-context-${TAB_ID}-copy-agent-id +- assertNotVisible: + id: workspace-tab-context-${TAB_ID}-copy-agent-id +- longPressOn: + id: workspace-tab-agent_${AGENT_ID} +- assertVisible: + id: workspace-tab-context-${TAB_ID}-copy-agent-id +- tapOn: + id: workspace-tab-context-${TAB_ID}-copy-agent-id +- assertNotVisible: + id: workspace-tab-context-${TAB_ID}-copy-agent-id +- tapOn: + id: sidebar-workspace-kebab-${WORKSPACE_KEY} +- tapOn: + id: sidebar-workspace-menu-rename-${WORKSPACE_KEY} +- assertVisible: + id: sidebar-workspace-rename-modal-${WORKSPACE_KEY} +- tapOn: + id: sidebar-workspace-rename-modal-${WORKSPACE_KEY}-cancel +- assertNotVisible: + id: sidebar-workspace-rename-modal-${WORKSPACE_KEY} +- tapOn: + id: sidebar-workspace-kebab-${WORKSPACE_KEY} +- assertVisible: + id: sidebar-workspace-menu-rename-${WORKSPACE_KEY} +- stopApp diff --git a/packages/app/e2e/mobile/agent-device/native-wide-overlay-dismissal.ios.yaml b/packages/app/e2e/mobile/agent-device/native-wide-overlay-dismissal.ios.yaml new file mode 100644 index 0000000000..1504548142 --- /dev/null +++ b/packages/app/e2e/mobile/agent-device/native-wide-overlay-dismissal.ios.yaml @@ -0,0 +1,40 @@ +appId: sh.paseo.debug +--- +- launchApp +- runFlow: + when: + visible: ".*getRegistrationInfoAsync.*" + commands: + - tapOn: ".*getRegistrationInfoAsync.*" + - tapOn: Dismiss +- assertNotVisible: ".*getRegistrationInfoAsync.*" +- extendedWaitUntil: + visible: + id: workspace-tabs-scroll + timeout: 30000 +- tapOn: + id: sidebar-workspace-kebab-${WORKSPACE_KEY} +- tapOn: + id: sidebar-workspace-menu-rename-${WORKSPACE_KEY} +- assertVisible: + id: sidebar-workspace-rename-modal-${WORKSPACE_KEY} +- tapOn: + id: sidebar-workspace-rename-modal-${WORKSPACE_KEY}-cancel +- runFlow: + when: + visible: + id: sidebar-workspace-rename-modal-${WORKSPACE_KEY} + commands: + - tapOn: + id: sidebar-workspace-rename-modal-${WORKSPACE_KEY}-cancel +- extendedWaitUntil: + notVisible: + id: sidebar-workspace-rename-modal-${WORKSPACE_KEY} + timeout: 10000 +- assertNotVisible: + id: sidebar-workspace-rename-modal-${WORKSPACE_KEY} +- tapOn: + id: sidebar-workspace-kebab-${WORKSPACE_KEY} +- assertVisible: + id: sidebar-workspace-menu-rename-${WORKSPACE_KEY} +- stopApp diff --git a/packages/app/e2e/mobile/agent-device/native-wide-sidebar-rename.android.ad b/packages/app/e2e/mobile/agent-device/native-wide-sidebar-rename.android.ad new file mode 100644 index 0000000000..49a070e02b --- /dev/null +++ b/packages/app/e2e/mobile/agent-device/native-wide-sidebar-rename.android.ad @@ -0,0 +1,13 @@ +context platform=android timeout=60000 retries=0 +env APP_ID=sh.paseo.debug + +open "${APP_ID}" +wait "id=\"workspace-tabs-scroll\"" 20000 +wait "id=\"sidebar-workspace-kebab-${WORKSPACE_KEY}\"" 20000 +press "id=\"sidebar-workspace-kebab-${WORKSPACE_KEY}\"" +wait "id=\"sidebar-workspace-menu-rename-${WORKSPACE_KEY}\"" 10000 +press "id=\"sidebar-workspace-menu-rename-${WORKSPACE_KEY}\"" +wait "id=\"sidebar-workspace-rename-modal-${WORKSPACE_KEY}\"" 10000 +press "id=\"sidebar-workspace-rename-modal-${WORKSPACE_KEY}-cancel\"" +wait "id=\"workspace-tabs-scroll\"" 10000 +close diff --git a/packages/app/e2e/mobile/agent-device/native-wide-sidebar-rename.ios.ad b/packages/app/e2e/mobile/agent-device/native-wide-sidebar-rename.ios.ad new file mode 100644 index 0000000000..ad637ca078 --- /dev/null +++ b/packages/app/e2e/mobile/agent-device/native-wide-sidebar-rename.ios.ad @@ -0,0 +1,16 @@ +context platform=ios timeout=60000 retries=0 +env APP_ID=sh.paseo.debug + +open "${APP_ID}" +wait "id=\"workspace-tabs-scroll\"" 20000 +wait "id=\"sidebar-workspace-kebab-${WORKSPACE_KEY}\"" 20000 +press "id=\"sidebar-workspace-kebab-${WORKSPACE_KEY}\"" +wait "id=\"sidebar-workspace-menu-rename-${WORKSPACE_KEY}\"" 10000 +press "id=\"sidebar-workspace-menu-rename-${WORKSPACE_KEY}\"" +wait "id=\"sidebar-workspace-rename-modal-${WORKSPACE_KEY}\"" 10000 +press "id=\"sidebar-workspace-rename-modal-${WORKSPACE_KEY}-cancel\"" +press "id=\"sidebar-workspace-rename-modal-${WORKSPACE_KEY}-cancel\"" +wait "id=\"sidebar-workspace-kebab-${WORKSPACE_KEY}\"" 10000 +press "id=\"sidebar-workspace-kebab-${WORKSPACE_KEY}\"" +wait "id=\"sidebar-workspace-menu-rename-${WORKSPACE_KEY}\"" 10000 +close diff --git a/packages/app/e2e/mobile/agent-device/native-wide-tab-scroll.android.ad b/packages/app/e2e/mobile/agent-device/native-wide-tab-scroll.android.ad new file mode 100644 index 0000000000..5016b8f576 --- /dev/null +++ b/packages/app/e2e/mobile/agent-device/native-wide-tab-scroll.android.ad @@ -0,0 +1,17 @@ +context platform=android timeout=60000 retries=0 +env APP_ID=sh.paseo.debug + +open "${APP_ID}" +wait "id=\"workspace-tabs-scroll\"" 20000 +gesture pan 900 155 550 0 280 +press "id=\"workspace-tab-draft_${DRAFT_ID}\"" +wait "id=\"composer-import-agent-pill\"" 10000 +gesture pan 770 155 -310 0 280 +is visible "id=\"composer-import-agent-pill\"" +gesture pan 900 155 550 0 280 +longpress "id=\"workspace-tab-agent_${AGENT_ID}\"" 900 +wait "id=\"workspace-tab-context-${TAB_ID}-copy-agent-id\"" 10000 +press "label=\"Menu backdrop\"" +press "id=\"workspace-tab-agent_${OTHER_AGENT_ID}\"" +wait "id=\"agent-chat-scroll\"" 10000 +close diff --git a/packages/app/e2e/mobile/agent-device/native-wide-tab-scroll.ios.ad b/packages/app/e2e/mobile/agent-device/native-wide-tab-scroll.ios.ad new file mode 100644 index 0000000000..dcf10f72a1 --- /dev/null +++ b/packages/app/e2e/mobile/agent-device/native-wide-tab-scroll.ios.ad @@ -0,0 +1,16 @@ +context platform=ios timeout=60000 retries=0 +env APP_ID=sh.paseo.debug + +open "${APP_ID}" +wait "id=\"workspace-tabs-scroll\"" 20000 +press "id=\"workspace-tab-draft_${DRAFT_ID}\"" +wait "id=\"composer-import-agent-pill\"" 10000 +gesture pan 450 86 -210 0 280 +is visible "id=\"composer-import-agent-pill\"" +gesture pan 450 86 240 0 280 +longpress "id=\"workspace-tab-agent_${AGENT_ID}\"" 900 +wait "id=\"workspace-tab-context-${TAB_ID}-copy-agent-id\"" 10000 +press "label=\"Menu backdrop\"" +press "id=\"workspace-tab-agent_${OTHER_AGENT_ID}\"" +wait "id=\"agent-chat-scroll\"" 10000 +close diff --git a/packages/app/e2e/mobile/agent-device/native-wide-tooltip-dismissal.android.yaml b/packages/app/e2e/mobile/agent-device/native-wide-tooltip-dismissal.android.yaml new file mode 100644 index 0000000000..17413bf207 --- /dev/null +++ b/packages/app/e2e/mobile/agent-device/native-wide-tooltip-dismissal.android.yaml @@ -0,0 +1,23 @@ +appId: sh.paseo.debug +--- +- launchApp +- extendedWaitUntil: + visible: + id: context-window-meter + timeout: 30000 +- tapOn: + id: context-window-meter +- assertVisible: + id: context-window-meter-tooltip +- tapOn: + id: tooltip-dismiss +- assertNotVisible: + id: context-window-meter-tooltip +- tapOn: + id: context-window-meter +- assertVisible: + id: context-window-meter-tooltip +- tapOn: + id: tooltip-dismiss +- assertNotVisible: + id: context-window-meter-tooltip diff --git a/packages/app/e2e/mobile/agent-device/native-wide-tooltip-dismissal.ios.yaml b/packages/app/e2e/mobile/agent-device/native-wide-tooltip-dismissal.ios.yaml new file mode 100644 index 0000000000..d048bf61d3 --- /dev/null +++ b/packages/app/e2e/mobile/agent-device/native-wide-tooltip-dismissal.ios.yaml @@ -0,0 +1,30 @@ +appId: sh.paseo.debug +--- +- launchApp +- runFlow: + when: + visible: ".*getRegistrationInfoAsync.*" + commands: + - tapOn: ".*getRegistrationInfoAsync.*" + - tapOn: Dismiss +- assertNotVisible: ".*getRegistrationInfoAsync.*" +- extendedWaitUntil: + visible: + id: context-window-meter + timeout: 30000 +- tapOn: + id: context-window-meter +- assertVisible: + id: context-window-meter-tooltip +- tapOn: + id: tooltip-dismiss +- assertNotVisible: + id: context-window-meter-tooltip +- tapOn: + id: context-window-meter +- assertVisible: + id: context-window-meter-tooltip +- tapOn: + id: tooltip-dismiss +- assertNotVisible: + id: context-window-meter-tooltip diff --git a/packages/app/src/components/context-window-meter.tsx b/packages/app/src/components/context-window-meter.tsx index 88c46a0150..2696f7c1ba 100644 --- a/packages/app/src/components/context-window-meter.tsx +++ b/packages/app/src/components/context-window-meter.tsx @@ -187,7 +187,7 @@ export function ContextWindowMeter({ ) : null} - + {t("contextWindow.title")} diff --git a/packages/app/src/components/ui/external-link.tsx b/packages/app/src/components/ui/external-link.tsx index 2902678f60..a29018b042 100644 --- a/packages/app/src/components/ui/external-link.tsx +++ b/packages/app/src/components/ui/external-link.tsx @@ -19,7 +19,7 @@ export interface ExternalLinkProps { /** * Inline "Docs ↗" affordance — muted text + arrow-top-right icon, opens the * URL via the platform's external opener. Wrap in a Tooltip when there's a - * one-line hint worth surfacing on hover/tap. + * one-line hint worth surfacing on hover. A native tap opens the URL. */ export function ExternalLink({ href, @@ -53,7 +53,7 @@ export function ExternalLink({ } return ( - + {trigger} {tooltip} diff --git a/packages/app/src/components/ui/tooltip.tsx b/packages/app/src/components/ui/tooltip.tsx index f69c4feab9..5e1221c169 100644 --- a/packages/app/src/components/ui/tooltip.tsx +++ b/packages/app/src/components/ui/tooltip.tsx @@ -28,7 +28,7 @@ import { FadeIn, FadeOut } from "react-native-reanimated"; import { StyleSheet } from "react-native-unistyles"; import { useIsCompactFormFactor } from "@/constants/layout"; import { FloatingSurface } from "@/components/ui/floating"; -import { isWeb } from "@/constants/platform"; +import { isNative, isWeb } from "@/constants/platform"; import { getOverlayRoot, OVERLAY_Z } from "@/lib/overlay-root"; type Side = "top" | "bottom" | "left" | "right"; @@ -248,7 +248,8 @@ export function Tooltip({ }); const isCompact = useIsCompactFormFactor(); - const enabled = isCompact ? enabledOnMobile : enabledOnDesktop; + const opensOnPress = isNative || isCompact; + const enabled = opensOnPress ? enabledOnMobile : enabledOnDesktop; const value = useMemo( () => ({ @@ -256,10 +257,10 @@ export function Tooltip({ setOpen: setIsOpen, triggerRef, enabled, - openOnPress: isCompact, + openOnPress: opensOnPress, delayDuration, }), - [isOpen, setIsOpen, enabled, isCompact, delayDuration], + [isOpen, setIsOpen, enabled, opensOnPress, delayDuration], ); return {children}; @@ -542,7 +543,7 @@ export function TooltipContent({ statusBarTranslucent={Platform.OS === "android"} onRequestClose={handleDismiss} > - + Date: Wed, 30 Sep 2026 13:56:29 +0100 Subject: [PATCH 021/100] fix(app): reflow iPad chat text after pane resize (#5716) The selectable UIKit text view was a sibling of the Fabric content container, so existing chat rows kept an old width after Explorer changed the available pane. Make the container own the text view and preserve layout notifications on bounds changes. Includes a native geometry regression. --- .../e2e/mobile/native-text-reflow/README.md | 26 +++++++ .../app/e2e/mobile/native-text-reflow/ios.py | 55 ++++++++++++++ .../app/e2e/mobile/native-text-reflow/ios.sh | 18 +++++ patches/react-native-uitextview+2.2.0.patch | 72 +++++++++++++++++++ scripts/postinstall-patches.mjs | 4 ++ 5 files changed, 175 insertions(+) create mode 100644 packages/app/e2e/mobile/native-text-reflow/README.md create mode 100644 packages/app/e2e/mobile/native-text-reflow/ios.py create mode 100644 packages/app/e2e/mobile/native-text-reflow/ios.sh create mode 100644 patches/react-native-uitextview+2.2.0.patch diff --git a/packages/app/e2e/mobile/native-text-reflow/README.md b/packages/app/e2e/mobile/native-text-reflow/README.md new file mode 100644 index 0000000000..b92666cc58 --- /dev/null +++ b/packages/app/e2e/mobile/native-text-reflow/README.md @@ -0,0 +1,26 @@ +# Native iOS text geometry regression + +Run on the Namespace Mac with a fresh simulator development build. Use the PID +returned by `xcrun simctl launch sh.paseo.debug`. +Attach only to your task's app, while no device replay is running: + +```sh +PASEO_NATIVE_TEXT_REFLOW_PID=12345 \ +PASEO_NATIVE_TEXT_REFLOW_LOG=/Volumes/devbox/workspace/native-text-reflow.log \ +bash packages/app/e2e/mobile/native-text-reflow/ios.sh +``` + +The probe constructs the installed `RNUITextView`, fills its real UIKit text view, +and resizes its content container from 500 to 240 and back to 500 points. It +asserts the inner frame follows both widths and the same selection survives. +It uses public UIView/UITextView operations, without mocking React Native. +LLDB detaches after the probe. An assertion failure or evaluation error fails the +shell command; the log distinguishes them. + +This covers native container resizing and selection retention. It does not cover +Fabric event delivery, chat row height, reader anchoring, or the on-screen copy +menu. Those require the real chat journey: read an older multiline response, +open/close Explorer, resize/rotate, inspect complete paragraphs and bullets, +retain expanded/collapsed details, and check a stationary reader's position. +Accessibility exposes full text even when glyphs are clipped, so a text wait +alone does not verify that journey. diff --git a/packages/app/e2e/mobile/native-text-reflow/ios.py b/packages/app/e2e/mobile/native-text-reflow/ios.py new file mode 100644 index 0000000000..7a4cbd5125 --- /dev/null +++ b/packages/app/e2e/mobile/native-text-reflow/ios.py @@ -0,0 +1,55 @@ +import lldb + + +def __lldb_init_module(debugger, _internal_dict): + debugger.HandleCommand("expression -l objc++ -- @import UIKit") + frame = debugger.GetSelectedTarget().GetProcess().GetSelectedThread().GetSelectedFrame() + options = lldb.SBExpressionOptions() + options.SetLanguage(lldb.eLanguageTypeObjC_plus_plus) + result = frame.EvaluateExpression( + r'''({ + UIView *host = (UIView *)[[NSClassFromString(@"RNUITextView") alloc] init]; + auto hostFrame = host.frame; + hostFrame.size.width = 500; + hostFrame.size.height = 200; + host.frame = hostFrame; + UIView *container = (UIView *)[host performSelector:@selector(contentView)]; + container.frame = host.bounds; + UITextView *text = nil; + for (UIView *child in host.subviews) { + if ([child isKindOfClass:UITextView.class]) text = (UITextView *)child; + for (UIView *nested in child.subviews) { + if ([nested isKindOfClass:UITextView.class]) text = (UITextView *)nested; + } + } + text.frame = container.bounds; + text.font = [UIFont systemFontOfSize:16]; + text.text = @"A realistic multiline paragraph must reflow when Explorer narrows the available chat width, then fill the restored width without replacing the text view or discarding a selection."; + auto range = text.selectedRange; + range.location = 12; + range.length = 9; + text.selectedRange = range; + auto frame = container.frame; + frame.size.width = 240; + container.frame = frame; + [container layoutIfNeeded]; + CGFloat narrow = text.frame.size.width; + auto selection = text.selectedRange; + frame.size.width = 500; + container.frame = frame; + [container layoutIfNeeded]; + CGFloat wide = text.frame.size.width; + BOOL ok = text != nil && narrow == 240 && wide == 500 && + selection.location == text.selectedRange.location && + selection.length == text.selectedRange.length && + selection.location == 12 && selection.length == 9; + [NSString stringWithFormat:@"NATIVE_REFLOW_%@ narrow=%.0f wide=%.0f selection=%lu:%lu", + ok ? @"PASS" : @"FAIL", narrow, wide, + (unsigned long)text.selectedRange.location, + (unsigned long)text.selectedRange.length]; + })''', + options, + ) + if result.GetError().Fail(): + raise RuntimeError(result.GetError().GetCString()) + print(result.GetObjectDescription()) diff --git a/packages/app/e2e/mobile/native-text-reflow/ios.sh b/packages/app/e2e/mobile/native-text-reflow/ios.sh new file mode 100644 index 0000000000..b4e9a07a22 --- /dev/null +++ b/packages/app/e2e/mobile/native-text-reflow/ios.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +set -euo pipefail + +: "${PASEO_NATIVE_TEXT_REFLOW_PID:?Set the PID of your task-owned Paseo iOS simulator app}" +: "${PASEO_NATIVE_TEXT_REFLOW_LOG:?Set the test output log path}" + +if [[ "$(ps -p "$PASEO_NATIVE_TEXT_REFLOW_PID" -o comm=)" != *PaseoDebug.app/PaseoDebug ]]; then + echo "Expected a running PaseoDebug simulator app" >&2 + exit 1 +fi + +probe_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +xcrun lldb --batch -p "$PASEO_NATIVE_TEXT_REFLOW_PID" \ + -o "command script import $probe_dir/ios.py" \ + -o "process detach" -o quit > "$PASEO_NATIVE_TEXT_REFLOW_LOG" 2>&1 + +grep '^NATIVE_REFLOW_' "$PASEO_NATIVE_TEXT_REFLOW_LOG" +grep -q '^NATIVE_REFLOW_PASS narrow=240 wide=500 selection=12:9$' "$PASEO_NATIVE_TEXT_REFLOW_LOG" diff --git a/patches/react-native-uitextview+2.2.0.patch b/patches/react-native-uitextview+2.2.0.patch new file mode 100644 index 0000000000..f64f09ebc7 --- /dev/null +++ b/patches/react-native-uitextview+2.2.0.patch @@ -0,0 +1,72 @@ +diff --git a/node_modules/react-native-uitextview/ios/RNUITextView.mm b/node_modules/react-native-uitextview/ios/RNUITextView.mm +index d0c31ed..df8a233 100644 +--- a/node_modules/react-native-uitextview/ios/RNUITextView.mm ++++ b/node_modules/react-native-uitextview/ios/RNUITextView.mm +@@ -22,6 +22,7 @@ using namespace facebook::react; + RNUITextViewShadowNode::ConcreteState::Shared _state; + UITapGestureRecognizer * _outsideTapRecognizer; + BOOL _suppressSelectionChange; ++ CGRect _lastTextLayoutFrame; + } + + + (ComponentDescriptorProvider)componentDescriptorProvider +@@ -47,7 +48,8 @@ using namespace facebook::react; + _textView.delegate = self; + // Must match RCTTextLayoutManager, which measures with usesFontLeading = NO. + _textView.layoutManager.usesFontLeading = NO; +- [self addSubview:_textView]; ++ _textView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; ++ [_view addSubview:_textView]; + + const auto longPressGestureRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self + action:@selector(handleLongPressIfNecessary:)]; +@@ -94,16 +96,15 @@ using namespace facebook::react; + // Reset the frame to zero so that when it properly lays out on the next use + _textView.frame = CGRectZero; + _textView.attributedText = nil; ++ _lastTextLayoutFrame = CGRectZero; + } + + - (void)layoutSubviews + { + [super layoutSubviews]; +- // _textView's frame is assigned inside drawRect, which only fires when +- // state changes. Trigger a redraw whenever the host frame moves out from +- // under it (rotation, parent relayout) so the text view resizes and +- // onTextLayout re-fires with the new line wrapping. +- if (!CGRectEqualToRect(_textView.frame, _view.frame)) { ++ // Autoresizing updates UIKit immediately; report the new line layout when ++ // the content container's bounds change. ++ if (!CGRectEqualToRect(_lastTextLayoutFrame, _view.bounds)) { + [self setNeedsDisplay]; + } + } +@@ -124,8 +125,9 @@ using namespace facebook::react; + // entirely when nothing actually changed so a JS-side state update made in + // response to onSelectionChange doesn't deselect what the user is selecting. + const BOOL textChanged = ![_textView.attributedText isEqualToAttributedString:convertedAttrString]; +- const BOOL frameChanged = !CGRectEqualToRect(_textView.frame, _view.frame); +- if (!textChanged && !frameChanged) { ++ const BOOL frameChanged = !CGRectEqualToRect(_textView.frame, _view.bounds); ++ const BOOL layoutChanged = !CGRectEqualToRect(_lastTextLayoutFrame, _view.bounds); ++ if (!textChanged && !frameChanged && !layoutChanged) { + return; + } + if (textChanged) { +@@ -142,7 +144,7 @@ using namespace facebook::react; + _suppressSelectionChange = NO; + } + if (frameChanged) { +- _textView.frame = _view.frame; ++ _textView.frame = _view.bounds; + } + + __block std::vector lines; +@@ -162,6 +164,7 @@ using namespace facebook::react; + } + }]; + ++ _lastTextLayoutFrame = _view.bounds; + if (_eventEmitter != nullptr) { + std::dynamic_pointer_cast(_eventEmitter) + ->onTextLayout(facebook::react::RNUITextViewEventEmitter::OnTextLayout{static_cast(self.tag), lines}); diff --git a/scripts/postinstall-patches.mjs b/scripts/postinstall-patches.mjs index 06d8892c60..4235127a63 100644 --- a/scripts/postinstall-patches.mjs +++ b/scripts/postinstall-patches.mjs @@ -12,6 +12,10 @@ const patchedPackages = [ nodeModulesPath: "node_modules/react-native-markdown-display", patchPrefix: "react-native-markdown-display+", }, + { + nodeModulesPath: "node_modules/react-native-uitextview", + patchPrefix: "react-native-uitextview+", + }, // Remove after react-native-unistyles ships // https://github.com/jpudysz/react-native-unistyles/pull/1203. { From 7b7685678db155bade64ec51d9c0f19e5f16a9e0 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Wed, 30 Sep 2026 14:09:07 +0100 Subject: [PATCH 022/100] feat(antigravity): add Antigravity provider plugin (#5714) * test(antigravity): record agy stream-json fixtures * feat(antigravity): add Antigravity provider plugin * fix(antigravity): address Phase 1 review findings * test(antigravity): record parity discovery blockers * feat(antigravity): support image prompts and daemon e2e * docs(antigravity): document provider setup and limitations * test(antigravity): respect Windows interrupt semantics * docs(plugins): avoid duplicate provider override guidance * fix(antigravity): report fresh denials and correct mode metadata Native denial results accumulate within a driver process. Track reported occurrences until respawn so later text-only turns do not repeat notices, while new denials of the same action remain visible. * fix(antigravity): use the official Antigravity mark --- docs/providers.md | 20 +- packages/server/package.json | 2 +- .../src/server/daemon-e2e/models.e2e.test.ts | 58 ++ .../src/server/plugins/builtin/index.ts | 1 + plugins/antigravity-provider/icon.svg | 3 + plugins/antigravity-provider/index.server.ts | 7 + .../antigravity-provider/paseo-plugin.json | 1 + .../server/internal/catalog.ts | 96 +++ .../server/internal/process.ts | 224 ++++++ .../server/internal/prompt.ts | 64 ++ .../server/internal/session.ts | 382 ++++++++++ .../server/internal/signals.ts | 40 + .../server/internal/timeline.ts | 55 ++ .../server/internal/wire.ts | 85 +++ .../server/provider.test.ts | 717 ++++++++++++++++++ .../antigravity-provider/server/provider.ts | 171 +++++ plugins/antigravity-provider/test/agy.mjs | 102 +++ .../test/fixtures/README.md | 100 +++ .../test/fixtures/agents.capture.ndjson | 2 + .../test/fixtures/agents.stderr.ndjson | 0 .../test/fixtures/agents.stdin.ndjson | 0 .../test/fixtures/agents.stdout.ndjson | 0 .../fixtures/api-key-env-only.capture.ndjson | 2 + .../fixtures/api-key-env-only.stderr.ndjson | 2 + .../fixtures/api-key-env-only.stdin.ndjson | 0 .../fixtures/api-key-env-only.stdout.ndjson | 1 + .../fixtures/api-key-settings.capture.ndjson | 2 + .../fixtures/api-key-settings.stderr.ndjson | 1 + .../fixtures/api-key-settings.stdin.ndjson | 1 + .../fixtures/api-key-settings.stdout.ndjson | 2 + .../cwd-without-add-dir.capture.ndjson | 2 + .../cwd-without-add-dir.stderr.ndjson | 0 .../fixtures/cwd-without-add-dir.stdin.ndjson | 1 + .../cwd-without-add-dir.stdout.ndjson | 11 + .../test/fixtures/denial-replay.stdout.ndjson | 11 + .../effort-encoded-match.capture.ndjson | 2 + .../effort-encoded-match.stderr.ndjson | 0 .../effort-encoded-match.stdin.ndjson | 1 + .../effort-encoded-match.stdout.ndjson | 19 + .../effort-encoded-mismatch.capture.ndjson | 3 + .../effort-encoded-mismatch.stderr.ndjson | 1 + .../effort-encoded-mismatch.stdin.ndjson | 0 .../effort-encoded-mismatch.stdout.ndjson | 1 + .../fixtures/effort-mismatch.capture.ndjson | 2 + .../fixtures/effort-mismatch.stderr.ndjson | 1 + .../fixtures/effort-mismatch.stdin.ndjson | 0 .../fixtures/effort-mismatch.stdout.ndjson | 1 + .../effort-unsupported-family.capture.ndjson | 2 + .../effort-unsupported-family.stderr.ndjson | 1 + .../effort-unsupported-family.stdin.ndjson | 0 .../effort-unsupported-family.stdout.ndjson | 1 + .../test/fixtures/help.capture.ndjson | 2 + .../test/fixtures/help.stderr.ndjson | 39 + .../test/fixtures/help.stdin.ndjson | 0 .../test/fixtures/help.stdout.ndjson | 0 .../fixtures/image-content.capture.ndjson | 2 + .../test/fixtures/image-content.stderr.ndjson | 1 + .../test/fixtures/image-content.stdin.ndjson | 1 + .../test/fixtures/image-content.stdout.ndjson | 2 + .../image-file-external.capture.ndjson | 2 + .../test/fixtures/image-file-external.png | Bin 0 -> 910 bytes .../image-file-external.stderr.ndjson | 0 .../fixtures/image-file-external.stdin.ndjson | 1 + .../image-file-external.stdout.ndjson | 14 + .../test/fixtures/mcp-add-http.capture.ndjson | 3 + .../test/fixtures/mcp-add-http.stderr.ndjson | 0 .../test/fixtures/mcp-add-http.stdin.ndjson | 0 .../test/fixtures/mcp-add-http.stdout.ndjson | 1 + .../test/fixtures/mcp-add.capture.ndjson | 2 + .../test/fixtures/mcp-add.stderr.ndjson | 1 + .../test/fixtures/mcp-add.stdin.ndjson | 0 .../test/fixtures/mcp-add.stdout.ndjson | 0 .../mcp-agent-definition.capture.ndjson | 2 + .../mcp-agent-definition.stderr.ndjson | 0 .../mcp-agent-definition.stdin.ndjson | 1 + .../mcp-agent-definition.stdout.ndjson | 15 + .../mcp-agent-discovered.capture.ndjson | 2 + .../mcp-agent-discovered.stderr.ndjson | 0 .../mcp-agent-discovered.stdin.ndjson | 1 + .../mcp-agent-discovered.stdout.ndjson | 19 + .../fixtures/mcp-agent-file.capture.ndjson | 2 + .../fixtures/mcp-agent-file.stderr.ndjson | 0 .../test/fixtures/mcp-agent-file.stdin.ndjson | 1 + .../fixtures/mcp-agent-file.stdout.ndjson | 39 + .../test/fixtures/mcp-global.capture.ndjson | 2 + .../test/fixtures/mcp-global.stderr.ndjson | 0 .../test/fixtures/mcp-global.stdin.ndjson | 1 + .../test/fixtures/mcp-global.stdout.ndjson | 14 + .../test/fixtures/mcp-help.capture.ndjson | 2 + .../test/fixtures/mcp-help.stderr.ndjson | 28 + .../test/fixtures/mcp-help.stdin.ndjson | 0 .../test/fixtures/mcp-help.stdout.ndjson | 0 .../test/fixtures/mcp-http.requests.ndjson | 20 + .../test/fixtures/mcp-list.capture.ndjson | 2 + .../test/fixtures/mcp-list.stderr.ndjson | 0 .../test/fixtures/mcp-list.stdin.ndjson | 0 .../test/fixtures/mcp-list.stdout.ndjson | 1 + .../fixtures/mcp-owned-cwd.capture.ndjson | 2 + .../test/fixtures/mcp-owned-cwd.stderr.ndjson | 0 .../test/fixtures/mcp-owned-cwd.stdin.ndjson | 1 + .../test/fixtures/mcp-owned-cwd.stdout.ndjson | 95 +++ .../mcp-owned-first-add-dir.capture.ndjson | 2 + .../mcp-owned-first-add-dir.stderr.ndjson | 0 .../mcp-owned-first-add-dir.stdin.ndjson | 1 + .../mcp-owned-first-add-dir.stdout.ndjson | 60 ++ .../mcp-project-config.capture.ndjson | 2 + .../fixtures/mcp-project-config.stderr.ndjson | 0 .../fixtures/mcp-project-config.stdin.ndjson | 1 + .../fixtures/mcp-project-config.stdout.ndjson | 5 + .../mcp-project-isolated.capture.ndjson | 4 + .../mcp-project-isolated.stderr.ndjson | 0 .../mcp-project-isolated.stdin.ndjson | 1 + .../mcp-project-isolated.stdout.ndjson | 31 + .../test/fixtures/mcp-project.capture.ndjson | 2 + .../test/fixtures/mcp-project.stderr.ndjson | 0 .../test/fixtures/mcp-project.stdin.ndjson | 1 + .../test/fixtures/mcp-project.stdout.ndjson | 15 + .../mcp-second-add-dir.capture.ndjson | 2 + .../fixtures/mcp-second-add-dir.stderr.ndjson | 0 .../fixtures/mcp-second-add-dir.stdin.ndjson | 1 + .../fixtures/mcp-second-add-dir.stdout.ndjson | 71 ++ .../model-switch-invalid.capture.ndjson | 3 + .../model-switch-invalid.stderr.ndjson | 13 + .../model-switch-invalid.stdin.ndjson | 0 .../model-switch-invalid.stdout.ndjson | 1 + .../model-switch-network.capture.ndjson | 3 + .../model-switch-network.stderr.ndjson | 0 .../model-switch-network.stdin.ndjson | 1 + .../model-switch-network.stdout.ndjson | 8 + .../model-switch-resume.capture.ndjson | 3 + .../model-switch-resume.stderr.ndjson | 13 + .../fixtures/model-switch-resume.stdin.ndjson | 0 .../model-switch-resume.stdout.ndjson | 1 + .../model-switch-valid.capture.ndjson | 2 + .../fixtures/model-switch-valid.stderr.ndjson | 0 .../fixtures/model-switch-valid.stdin.ndjson | 1 + .../fixtures/model-switch-valid.stdout.ndjson | 7 + .../fixtures/models-api-key.capture.ndjson | 2 + .../fixtures/models-api-key.stderr.ndjson | 1 + .../test/fixtures/models-api-key.stdin.ndjson | 0 .../fixtures/models-api-key.stdout.ndjson | 11 + .../models-unauthenticated.capture.ndjson | 2 + .../models-unauthenticated.stderr.ndjson | 2 + .../models-unauthenticated.stdin.ndjson | 0 .../models-unauthenticated.stdout.ndjson | 0 .../multi-turn-reasoning.capture.ndjson | 2 + .../multi-turn-reasoning.stderr.ndjson | 0 .../multi-turn-reasoning.stdin.ndjson | 2 + .../multi-turn-reasoning.stdout.ndjson | 51 ++ .../fixtures/not-signed-in.capture.ndjson | 2 + .../test/fixtures/not-signed-in.stderr.ndjson | 2 + .../test/fixtures/not-signed-in.stdin.ndjson | 0 .../test/fixtures/not-signed-in.stdout.ndjson | 1 + .../test/fixtures/parity-inspection.ndjson | 10 + .../fixtures/project-skill.capture.ndjson | 2 + .../test/fixtures/project-skill.stderr.ndjson | 0 .../test/fixtures/project-skill.stdin.ndjson | 1 + .../test/fixtures/project-skill.stdout.ndjson | 9 + .../question-auto-denial.capture.ndjson | 2 + .../question-auto-denial.stderr.ndjson | 0 .../question-auto-denial.stdin.ndjson | 1 + .../question-auto-denial.stdout.ndjson | 14 + .../resume-after-interrupt.capture.ndjson | 2 + .../resume-after-interrupt.stderr.ndjson | 0 .../resume-after-interrupt.stdin.ndjson | 1 + .../resume-after-interrupt.stdout.ndjson | 7 + .../test/fixtures/resume.capture.ndjson | 2 + .../test/fixtures/resume.stderr.ndjson | 0 .../test/fixtures/resume.stdin.ndjson | 1 + .../test/fixtures/resume.stdout.ndjson | 8 + .../fixtures/schema-prompt.capture.ndjson | 3 + .../test/fixtures/schema-prompt.stderr.ndjson | 1 + .../test/fixtures/schema-prompt.stdin.ndjson | 1 + .../test/fixtures/schema-prompt.stdout.ndjson | 1 + .../schema-user-message.capture.ndjson | 2 + .../schema-user-message.stderr.ndjson | 1 + .../fixtures/schema-user-message.stdin.ndjson | 1 + .../schema-user-message.stdout.ndjson | 2 + .../test/fixtures/schema-user.capture.ndjson | 2 + .../test/fixtures/schema-user.stderr.ndjson | 1 + .../test/fixtures/schema-user.stdin.ndjson | 1 + .../test/fixtures/schema-user.stdout.ndjson | 2 + .../fixtures/sigint-mid-text.capture.ndjson | 3 + .../fixtures/sigint-mid-text.stderr.ndjson | 1 + .../fixtures/sigint-mid-text.stdin.ndjson | 1 + .../fixtures/sigint-mid-text.stdout.ndjson | 4 + .../fixtures/sigint-mid-tool.capture.ndjson | 3 + .../fixtures/sigint-mid-tool.stderr.ndjson | 1 + .../fixtures/sigint-mid-tool.stdin.ndjson | 1 + .../fixtures/sigint-mid-tool.stdout.ndjson | 5 + .../test/fixtures/sigterm.capture.ndjson | 3 + .../test/fixtures/sigterm.stderr.ndjson | 1 + .../test/fixtures/sigterm.stdin.ndjson | 1 + .../test/fixtures/sigterm.stdout.ndjson | 3 + .../fixtures/slash-disabled.capture.ndjson | 2 + .../fixtures/slash-disabled.stderr.ndjson | 0 .../test/fixtures/slash-disabled.stdin.ndjson | 1 + .../fixtures/slash-disabled.stdout.ndjson | 84 ++ .../fixtures/slash-enabled.capture.ndjson | 2 + .../test/fixtures/slash-enabled.stderr.ndjson | 1 + .../test/fixtures/slash-enabled.stdin.ndjson | 1 + .../test/fixtures/slash-enabled.stdout.ndjson | 2 + .../test/fixtures/state-inspection.ndjson | 6 + .../stdin-close-mid-turn.capture.ndjson | 3 + .../stdin-close-mid-turn.stderr.ndjson | 0 .../stdin-close-mid-turn.stdin.ndjson | 1 + .../stdin-close-mid-turn.stdout.ndjson | 5 + .../fixtures/stdin-mid-turn.capture.ndjson | 2 + .../fixtures/stdin-mid-turn.stderr.ndjson | 0 .../test/fixtures/stdin-mid-turn.stdin.ndjson | 2 + .../fixtures/stdin-mid-turn.stdout.ndjson | 179 +++++ .../test/fixtures/subagent.capture.ndjson | 2 + .../test/fixtures/subagent.stderr.ndjson | 0 .../test/fixtures/subagent.stdin.ndjson | 1 + .../test/fixtures/subagent.stdout.ndjson | 23 + .../test/fixtures/text-block.capture.ndjson | 2 + .../test/fixtures/text-block.stderr.ndjson | 0 .../test/fixtures/text-block.stdin.ndjson | 1 + .../test/fixtures/text-block.stdout.ndjson | 5 + .../test/fixtures/text-turn.capture.ndjson | 2 + .../test/fixtures/text-turn.stderr.ndjson | 0 .../test/fixtures/text-turn.stdin.ndjson | 1 + .../test/fixtures/text-turn.stdout.ndjson | 5 + .../tools-accept-edits.capture.ndjson | 2 + .../fixtures/tools-accept-edits.stderr.ndjson | 1 + .../fixtures/tools-accept-edits.stdin.ndjson | 3 + .../fixtures/tools-accept-edits.stdout.ndjson | 40 + .../fixtures/tools-default.capture.ndjson | 2 + .../test/fixtures/tools-default.stderr.ndjson | 1 + .../test/fixtures/tools-default.stdin.ndjson | 3 + .../test/fixtures/tools-default.stdout.ndjson | 41 + .../test/fixtures/tools-plan.capture.ndjson | 2 + .../test/fixtures/tools-plan.stderr.ndjson | 4 + .../test/fixtures/tools-plan.stdin.ndjson | 3 + .../test/fixtures/tools-plan.stdout.ndjson | 36 + .../fixtures/tools-sandbox.capture.ndjson | 2 + .../test/fixtures/tools-sandbox.stderr.ndjson | 1 + .../test/fixtures/tools-sandbox.stdin.ndjson | 3 + .../test/fixtures/tools-sandbox.stdout.ndjson | 40 + .../tools-skip-permissions.capture.ndjson | 2 + .../tools-skip-permissions.stderr.ndjson | 0 .../tools-skip-permissions.stdin.ndjson | 3 + .../tools-skip-permissions.stdout.ndjson | 42 + .../test/fixtures/version.capture.ndjson | 2 + .../test/fixtures/version.stderr.ndjson | 0 .../test/fixtures/version.stdin.ndjson | 0 .../test/fixtures/version.stdout.ndjson | 1 + public-docs/supported-providers.md | 31 + 248 files changed, 3555 insertions(+), 3 deletions(-) create mode 100644 plugins/antigravity-provider/icon.svg create mode 100644 plugins/antigravity-provider/index.server.ts create mode 100644 plugins/antigravity-provider/paseo-plugin.json create mode 100644 plugins/antigravity-provider/server/internal/catalog.ts create mode 100644 plugins/antigravity-provider/server/internal/process.ts create mode 100644 plugins/antigravity-provider/server/internal/prompt.ts create mode 100644 plugins/antigravity-provider/server/internal/session.ts create mode 100644 plugins/antigravity-provider/server/internal/signals.ts create mode 100644 plugins/antigravity-provider/server/internal/timeline.ts create mode 100644 plugins/antigravity-provider/server/internal/wire.ts create mode 100644 plugins/antigravity-provider/server/provider.test.ts create mode 100644 plugins/antigravity-provider/server/provider.ts create mode 100644 plugins/antigravity-provider/test/agy.mjs create mode 100644 plugins/antigravity-provider/test/fixtures/README.md create mode 100644 plugins/antigravity-provider/test/fixtures/agents.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/agents.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/agents.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/agents.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/api-key-env-only.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/api-key-env-only.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/api-key-env-only.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/api-key-env-only.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/api-key-settings.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/api-key-settings.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/api-key-settings.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/api-key-settings.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/cwd-without-add-dir.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/cwd-without-add-dir.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/cwd-without-add-dir.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/cwd-without-add-dir.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/denial-replay.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/effort-encoded-match.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/effort-encoded-match.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/effort-encoded-match.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/effort-encoded-match.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/effort-encoded-mismatch.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/effort-encoded-mismatch.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/effort-encoded-mismatch.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/effort-encoded-mismatch.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/effort-mismatch.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/effort-mismatch.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/effort-mismatch.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/effort-mismatch.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/effort-unsupported-family.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/effort-unsupported-family.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/effort-unsupported-family.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/effort-unsupported-family.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/help.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/help.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/help.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/help.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/image-content.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/image-content.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/image-content.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/image-content.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/image-file-external.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/image-file-external.png create mode 100644 plugins/antigravity-provider/test/fixtures/image-file-external.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/image-file-external.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/image-file-external.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-add-http.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-add-http.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-add-http.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-add-http.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-add.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-add.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-add.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-add.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-agent-definition.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-agent-definition.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-agent-definition.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-agent-definition.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-agent-discovered.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-agent-discovered.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-agent-discovered.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-agent-discovered.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-agent-file.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-agent-file.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-agent-file.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-agent-file.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-global.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-global.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-global.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-global.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-help.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-help.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-help.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-help.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-http.requests.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-list.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-list.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-list.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-list.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-owned-cwd.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-owned-cwd.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-owned-cwd.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-owned-cwd.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-owned-first-add-dir.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-owned-first-add-dir.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-owned-first-add-dir.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-owned-first-add-dir.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-project-config.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-project-config.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-project-config.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-project-config.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-project-isolated.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-project-isolated.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-project-isolated.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-project-isolated.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-project.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-project.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-project.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-project.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-second-add-dir.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-second-add-dir.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-second-add-dir.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-second-add-dir.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/model-switch-invalid.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/model-switch-invalid.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/model-switch-invalid.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/model-switch-invalid.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/model-switch-network.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/model-switch-network.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/model-switch-network.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/model-switch-network.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/model-switch-resume.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/model-switch-resume.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/model-switch-resume.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/model-switch-resume.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/model-switch-valid.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/model-switch-valid.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/model-switch-valid.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/model-switch-valid.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/models-api-key.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/models-api-key.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/models-api-key.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/models-api-key.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/models-unauthenticated.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/models-unauthenticated.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/models-unauthenticated.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/models-unauthenticated.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/multi-turn-reasoning.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/multi-turn-reasoning.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/multi-turn-reasoning.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/multi-turn-reasoning.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/not-signed-in.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/not-signed-in.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/not-signed-in.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/not-signed-in.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/parity-inspection.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/project-skill.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/project-skill.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/project-skill.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/project-skill.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/question-auto-denial.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/question-auto-denial.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/question-auto-denial.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/question-auto-denial.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/resume-after-interrupt.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/resume-after-interrupt.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/resume-after-interrupt.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/resume-after-interrupt.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/resume.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/resume.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/resume.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/resume.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/schema-prompt.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/schema-prompt.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/schema-prompt.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/schema-prompt.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/schema-user-message.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/schema-user-message.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/schema-user-message.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/schema-user-message.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/schema-user.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/schema-user.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/schema-user.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/schema-user.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/sigint-mid-text.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/sigint-mid-text.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/sigint-mid-text.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/sigint-mid-text.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/sigint-mid-tool.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/sigint-mid-tool.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/sigint-mid-tool.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/sigint-mid-tool.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/sigterm.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/sigterm.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/sigterm.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/sigterm.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/slash-disabled.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/slash-disabled.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/slash-disabled.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/slash-disabled.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/slash-enabled.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/slash-enabled.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/slash-enabled.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/slash-enabled.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/state-inspection.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/stdin-close-mid-turn.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/stdin-close-mid-turn.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/stdin-close-mid-turn.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/stdin-close-mid-turn.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/stdin-mid-turn.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/stdin-mid-turn.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/stdin-mid-turn.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/stdin-mid-turn.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/subagent.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/subagent.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/subagent.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/subagent.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/text-block.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/text-block.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/text-block.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/text-block.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/text-turn.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/text-turn.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/text-turn.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/text-turn.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/tools-accept-edits.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/tools-accept-edits.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/tools-accept-edits.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/tools-accept-edits.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/tools-default.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/tools-default.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/tools-default.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/tools-default.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/tools-plan.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/tools-plan.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/tools-plan.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/tools-plan.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/tools-sandbox.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/tools-sandbox.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/tools-sandbox.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/tools-sandbox.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/tools-skip-permissions.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/tools-skip-permissions.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/tools-skip-permissions.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/tools-skip-permissions.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/version.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/version.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/version.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/version.stdout.ndjson diff --git a/docs/providers.md b/docs/providers.md index 87cdf527d2..d44a886674 100644 --- a/docs/providers.md +++ b/docs/providers.md @@ -1,6 +1,22 @@ # Adding a New Provider to Paseo -This guide walks through adding a new agent provider end-to-end. There are two integration patterns, and this doc covers both. +Add new providers through the plugin SDK. The core adapter patterns below describe the existing +server integrations. + +## Plugin providers + +Keep a bundled provider in `plugins//` and register it through +`@getpaseo/plugin/server/provider`. Antigravity follows this pattern. Built-in loading and SDK +import rules belong to [plugins.md](plugins.md#built-in-plugins); the +[public provider guide](../public-docs/plugins/providers.md) covers the provider contract. + +The plugin owns the CLI transport, session state, catalog, and capabilities. The daemon owns +executable resolution and applies `agents.providers..command` and `env` before +connecting. Register the provider's icon with the plugin rather than adding it to the app's +provider icon map. You do not need a core manifest entry or provider factory. + +For Antigravity installation, modes, and limitations, see +[Supported providers](../public-docs/supported-providers.md#antigravity). ## Provider-native session options @@ -46,7 +62,7 @@ Each provider definition owns its option schema and exact MCP preapproval mappin must fail closed for Hub unattended execution until it can approve one exact injected MCP server and tool identity without approving native tools. -## Two Integration Patterns +## Core adapter patterns ### ACP (Agent Client Protocol) -- recommended diff --git a/packages/server/package.json b/packages/server/package.json index 047c7b72a1..e3c5562035 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -73,7 +73,7 @@ "clean": "node ../../scripts/clean-package-dist.mjs", "build": "npm run build:lib && npm run build:scripts", "build:clean": "npm run clean && npm run build", - "build:lib": "tsc -p tsconfig.server.json --incremental false && node scripts/build-opencode-bridge-plugin.mjs && node -e \"const fs=require('node:fs'); fs.mkdirSync('dist/server/server/speech/providers/local/sherpa/assets',{recursive:true}); fs.copyFileSync('src/server/speech/providers/local/sherpa/assets/silero_vad.onnx','dist/server/server/speech/providers/local/sherpa/assets/silero_vad.onnx'); fs.cpSync('src/terminal/shell-integration','dist/server/terminal/shell-integration',{recursive:true}); fs.cpSync('src/terminal/shell-integration','dist/src/terminal/shell-integration',{recursive:true}); fs.copyFileSync('src/terminal/terminal-ts-loader.mjs','dist/server/terminal/terminal-ts-loader.mjs'); fs.rmSync('dist/server/skills',{recursive:true,force:true}); fs.cpSync('../../skills','dist/server/skills',{recursive:true}); fs.rmSync('dist/server/builtin-plugins',{recursive:true,force:true}); fs.cpSync('../../plugins','dist/server/builtin-plugins',{recursive:true,filter:(source)=>!source.split(require('node:path').sep).includes('node_modules')&&!/\\.(test|spec)\\.[cm]?[jt]sx?$/.test(source)&&!source.endsWith('.tsbuildinfo')});\"", + "build:lib": "tsc -p tsconfig.server.json --incremental false && node scripts/build-opencode-bridge-plugin.mjs && node -e \"const fs=require('node:fs'); fs.mkdirSync('dist/server/server/speech/providers/local/sherpa/assets',{recursive:true}); fs.copyFileSync('src/server/speech/providers/local/sherpa/assets/silero_vad.onnx','dist/server/server/speech/providers/local/sherpa/assets/silero_vad.onnx'); fs.cpSync('src/terminal/shell-integration','dist/server/terminal/shell-integration',{recursive:true}); fs.cpSync('src/terminal/shell-integration','dist/src/terminal/shell-integration',{recursive:true}); fs.copyFileSync('src/terminal/terminal-ts-loader.mjs','dist/server/terminal/terminal-ts-loader.mjs'); fs.rmSync('dist/server/skills',{recursive:true,force:true}); fs.cpSync('../../skills','dist/server/skills',{recursive:true}); fs.rmSync('dist/server/builtin-plugins',{recursive:true,force:true}); fs.cpSync('../../plugins','dist/server/builtin-plugins',{recursive:true,filter:(source)=>!source.split(require('node:path').sep).some((segment)=>segment==='node_modules'||segment==='test')&&!/\\.(test|spec)\\.[cm]?[jt]sx?$/.test(source)&&!source.endsWith('.tsbuildinfo')});\"", "build:scripts": "tsc -p tsconfig.scripts.json --incremental false && node -e \"const fs=require('node:fs'); fs.mkdirSync('dist/scripts',{recursive:true}); fs.copyFileSync('scripts/mcp-stdio-socket-bridge-cli.mjs','dist/scripts/mcp-stdio-socket-bridge-cli.mjs');\"", "prepack": "npm run build:clean && npm --prefix ../.. run build:daemon-web-ui", "start": "node dist/scripts/supervisor-entrypoint.js", diff --git a/packages/server/src/server/daemon-e2e/models.e2e.test.ts b/packages/server/src/server/daemon-e2e/models.e2e.test.ts index 7691e63713..ff93b695c1 100644 --- a/packages/server/src/server/daemon-e2e/models.e2e.test.ts +++ b/packages/server/src/server/daemon-e2e/models.e2e.test.ts @@ -1,4 +1,11 @@ import { describe, test, expect } from "vitest"; +import { mkdtemp, rm } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { findExecutable } from "../../executable-resolution/executable-resolution.js"; +import { BuiltinPluginLoader } from "../plugins/builtin/index.js"; +import { createTestPaseoDaemon } from "../test-utils/paseo-daemon.js"; +import { DaemonClient } from "../test-utils/daemon-client.js"; import { execFileSync } from "node:child_process"; import { createDaemonTestContext } from "../test-utils/index.js"; @@ -96,3 +103,54 @@ describe("daemon E2E", () => { ); }); }); + +test("Antigravity creates an agent and answers a prompt with real agy", async ({ skip }) => { + const command = await findExecutable(process.env.AGY_COMMAND ?? "agy"); + if (command === null) { + skip("agy is not resolvable"); + return; + } + const cwd = await mkdtemp(path.join(os.tmpdir(), "paseo-antigravity-e2e-")); + const daemon = await createTestPaseoDaemon({ + agentClients: {}, + mcpEnabled: false, + builtinPlugins: new BuiltinPluginLoader(undefined, ["antigravity-provider"]), + providerOverrides: { antigravity: { command: [command], paseoTools: { enabled: false } } }, + }); + const client = new DaemonClient({ + url: `ws://127.0.0.1:${daemon.port}/ws`, + appVersion: "0.10.0", + }); + try { + await client.connect(); + await client.fetchAgents({ subscribe: {} }); + await expect + .poll( + async () => { + const snapshot = await client.getProvidersSnapshot({ cwd }); + return snapshot.entries.find((entry) => entry.provider === "antigravity")?.status; + }, + { timeout: 30000 }, + ) + .toBe("ready"); + const agent = await client.createAgent({ + provider: "antigravity", + cwd, + title: "Antigravity E2E", + }); + await client.sendMessage(agent.id, "Reply with exactly ANTIGRAVITY_E2E_OK. No tools."); + const result = await client.waitForFinish(agent.id, 90000); + expect(result.status).toBe("idle"); + const timeline = await client.fetchAgentTimeline(agent.id, { limit: 100 }); + expect( + timeline.entries.some( + (entry) => + entry.item.type === "assistant_message" && entry.item.text.includes("ANTIGRAVITY_E2E_OK"), + ), + ).toBe(true); + } finally { + await client.close(); + await daemon.close(); + await rm(cwd, { recursive: true, force: true }); + } +}, 120000); diff --git a/packages/server/src/server/plugins/builtin/index.ts b/packages/server/src/server/plugins/builtin/index.ts index de9b66131e..52f9c9df3c 100644 --- a/packages/server/src/server/plugins/builtin/index.ts +++ b/packages/server/src/server/plugins/builtin/index.ts @@ -3,6 +3,7 @@ import path from "node:path"; import { fileURLToPath } from "node:url"; export const builtinPlugins = [ + "antigravity-provider", "claude-usage-source", "codex-usage-source", "copilot-usage-source", diff --git a/plugins/antigravity-provider/icon.svg b/plugins/antigravity-provider/icon.svg new file mode 100644 index 0000000000..0576d92a87 --- /dev/null +++ b/plugins/antigravity-provider/icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/plugins/antigravity-provider/index.server.ts b/plugins/antigravity-provider/index.server.ts new file mode 100644 index 0000000000..661fa8445d --- /dev/null +++ b/plugins/antigravity-provider/index.server.ts @@ -0,0 +1,7 @@ +import type { PluginServerContext } from "@getpaseo/plugin/server"; +import { createAntigravityProvider } from "./server/provider.js"; + +export default function contribute(server: PluginServerContext) { + server.registerProvider(createAntigravityProvider()); + return () => {}; +} diff --git a/plugins/antigravity-provider/paseo-plugin.json b/plugins/antigravity-provider/paseo-plugin.json new file mode 100644 index 0000000000..ce6b2834f2 --- /dev/null +++ b/plugins/antigravity-provider/paseo-plugin.json @@ -0,0 +1 @@ +{ "id": "antigravity-provider", "requirements": { "paseo": ">=0.10.0" } } diff --git a/plugins/antigravity-provider/server/internal/catalog.ts b/plugins/antigravity-provider/server/internal/catalog.ts new file mode 100644 index 0000000000..74e3be583e --- /dev/null +++ b/plugins/antigravity-provider/server/internal/catalog.ts @@ -0,0 +1,96 @@ +import type { + ProviderCatalog, + ProviderLaunch, + ProviderModel, + ProviderMode, + ProviderStatus, +} from "@getpaseo/plugin/server/provider"; +import { probe } from "./process.js"; +import { AntigravityError } from "./wire.js"; + +const modes: readonly ProviderMode[] = [ + { + id: "default", + icon: "Shield", + colorTier: "moderate", + label: "Default", + description: "Workspace reads and edits are allowed; shell commands are denied.", + }, + { + id: "accept-edits", + icon: "ShieldPlus", + colorTier: "moderate", + label: "Accept edits", + description: + "Native accept-edits mode. Workspace edits are allowed; shell commands are denied.", + }, + { + id: "plan", + icon: "ShieldEllipsis", + colorTier: "planning", + label: "Plan", + description: + "Native plan mode permits workspace edits. Shell commands are denied; this is not read-only.", + }, + { + id: "full-access", + icon: "ShieldOff", + colorTier: "dangerous", + label: "Full access", + description: "Skip native permission checks, including for shell commands.", + isUnattended: true, + }, +]; + +export async function getCatalog(launch: ProviderLaunch, cwd?: string): Promise { + const output = await probe({ launch, args: ["models"], cwd }); + const models: ProviderModel[] = []; + for (const line of output.trim().split(/\r?\n/)) { + const [id, label, ...extra] = line.split("\t"); + if (!id || !label || extra.length > 0) + throw new AntigravityError("Invalid `agy models` output", "INVALID_CATALOG"); + if (models.some((model) => model.id === id)) + throw new AntigravityError(`Duplicate Antigravity model: ${id}`, "INVALID_CATALOG"); + models.push({ id, label }); + } + return { models, modes, thinkingOptions: [], defaultMode: "default" }; +} + +export async function getStatus(launch: ProviderLaunch): Promise { + try { + const version = (await probe({ launch, args: ["--version"] })).trim(); + const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(version); + if (!match) + return { + available: false, + diagnostic: "Could not read the Antigravity version. Update `agy` to 1.1.15 or later.", + }; + const [, major, minor, patch] = match.map(Number); + const supported = major > 1 || (major === 1 && (minor > 1 || (minor === 1 && patch >= 15))); + if (!supported) + return { + available: false, + diagnostic: `Antigravity ${version} is unsupported. Update \`agy\` to 1.1.15 or later.`, + }; + await getCatalog(launch); + return { available: true }; + } catch (error) { + if (!(error instanceof AntigravityError)) throw error; + return { available: false, diagnostic: error.message }; + } +} + +export function validateSelection( + config: { model?: string; mode?: string; thinkingOption?: string }, + catalog: ProviderCatalog, +): void { + if (config.model && !catalog.models.some((model) => model.id === config.model)) + throw new AntigravityError(`Unknown Antigravity model: ${config.model}`, "INVALID_MODEL"); + if (config.mode && !catalog.modes.some((mode) => mode.id === config.mode)) + throw new AntigravityError(`Unknown Antigravity mode: ${config.mode}`, "INVALID_MODE"); + if (config.thinkingOption) + throw new AntigravityError( + "Antigravity effort is selected through the model ID", + "UNSUPPORTED_THINKING", + ); +} diff --git a/plugins/antigravity-provider/server/internal/process.ts b/plugins/antigravity-provider/server/internal/process.ts new file mode 100644 index 0000000000..a1b2f1c7b8 --- /dev/null +++ b/plugins/antigravity-provider/server/internal/process.ts @@ -0,0 +1,224 @@ +import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process"; +import { signalProcess } from "./signals.js"; +import { createInterface } from "node:readline"; +import type { ProviderLaunch, ProviderSessionConfig } from "@getpaseo/plugin/server/provider"; +import { + AntigravityError, + decodeFrame, + diagnostic, + encodePrompt, + type Frame, + type Init, +} from "./wire.js"; + +interface DriverOptions { + launch: ProviderLaunch; + config: ProviderSessionConfig; + conversationId: string | null; + onFrame(frame: Frame): void; + onExit(error: AntigravityError): void; +} + +export interface Driver { + ready: Promise; + selection: string; + prompt(text: string): Promise; + stop(reason: "interrupt" | "close"): Promise; +} + +export function selection(config: ProviderSessionConfig): string { + return JSON.stringify([config.model, config.mode]); +} + +function driverArgs(options: DriverOptions): string[] { + const { config, conversationId } = options; + const args = [ + ...options.launch.args, + "--input-format", + "stream-json", + "--output-format", + "stream-json", + "--add-dir", + config.cwd, + "--disable-slash-commands", + ]; + if (config.model) args.push("--model", config.model); + if (config.mode === "accept-edits" || config.mode === "plan") args.push("--mode", config.mode); + if (config.mode === "full-access") args.push("--dangerously-skip-permissions"); + if (conversationId) args.push("--conversation", conversationId); + return args; +} + +export function startDriver(options: DriverOptions): Driver { + const child = spawn(options.launch.command, driverArgs(options), { + cwd: options.config.cwd, + env: { ...options.launch.env, ...options.config.env }, + detached: process.platform !== "win32", + stdio: "pipe", + }); + const lines = createInterface({ input: child.stdout }); + let cleanup = Promise.resolve(); + let stderr = ""; + let state: "starting" | "ready" | "stopping" | "exited" = "starting"; + let resolveReady: (init: Init) => void; + let rejectReady: (error: Error) => void; + const ready = new Promise((resolve, reject) => { + resolveReady = resolve; + rejectReady = reject; + }); + let resolveExit: () => void; + const exited = new Promise((resolve) => { + resolveExit = resolve; + }); + const startupDeadline = setTimeout( + () => + fail( + new AntigravityError("Antigravity did not initialize within 30 seconds", "STARTUP_TIMEOUT"), + ), + 30_000, + ); + child.stderr.on("data", (chunk: Buffer) => { + stderr = (stderr + chunk.toString()).slice(-8192); + }); + child.stdin.on("error", (error: Error) => + fail(new AntigravityError(error.message, "STDIN_ERROR")), + ); + child.on("error", (error: Error) => fail(new AntigravityError(error.message, "SPAWN_ERROR"))); + child.on("close", (code, signal) => { + clearTimeout(startupDeadline); + lines.close(); + const error = new AntigravityError( + diagnostic(stderr.trim() || `Antigravity exited (${signal || code})`), + "PROCESS_EXIT", + ); + if (state === "starting") rejectReady(error); + if (state === "ready") options.onExit(error); + state = "exited"; + resolveExit(); + }); + lines.on("line", (line) => { + if (!line.trim() || state === "exited" || state === "stopping") return; + let decoded: Frame; + try { + decoded = decodeFrame(line); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + fail(new AntigravityError(`Invalid Antigravity output: ${message}`, "INVALID_FRAME")); + return; + } + if (state === "starting") { + if (decoded.event === "init") { + clearTimeout(startupDeadline); + state = "ready"; + resolveReady(decoded); + } else if (decoded.event === "result") { + fail(new AntigravityError(diagnostic(decoded.result.error), "STARTUP_ERROR")); + } + return; + } + options.onFrame(decoded); + }); + + function fail(failure: AntigravityError): void { + clearTimeout(startupDeadline); + if (state === "starting") rejectReady(failure); + if (state === "ready") options.onExit(failure); + if (state === "exited" || state === "stopping") return; + state = "stopping"; + cleanup = Promise.resolve(signalGroup(child, "SIGKILL")); + // The stop operation awaits cleanup and owns reporting its failure. + void cleanup.catch(() => undefined); + } + + return { + ready, + selection: selection(options.config), + prompt(text) { + if (state !== "ready") + return Promise.reject(new AntigravityError("Antigravity is not ready")); + return new Promise((resolve, reject) => { + child.stdin.write(encodePrompt(text), (error) => { + if (error) reject(error); + else resolve(); + }); + }); + }, + async stop(reason) { + if (process.platform === "win32") await cleanup; + if (state === "exited") return; + if (state === "starting") rejectReady(new AntigravityError("Antigravity startup canceled")); + clearTimeout(startupDeadline); + state = "stopping"; + if (process.platform === "win32") { + // Node signal emulation kills only the leader; taskkill must see the live tree. + cleanup = Promise.resolve(signalGroup(child, "SIGKILL")); + await cleanup; + await exited; + return; + } + if (reason === "close") child.stdin.end(); + else signalGroup(child, "SIGINT"); + const terminate = setTimeout(() => signalGroup(child, "SIGTERM"), 1000); + const kill = setTimeout(() => signalGroup(child, "SIGKILL"), 2000); + await exited; + clearTimeout(terminate); + clearTimeout(kill); + // A tool can keep running after its CLI leader exits. + signalGroup(child, "SIGKILL"); + }, + }; +} + +function signalGroup( + child: ChildProcessWithoutNullStreams, + signal: NodeJS.Signals, +): void | Promise { + if (!child.pid) return; + return signalProcess({ platform: process.platform, pid: child.pid, signal }); +} + +interface ProbeOptions { + launch: ProviderLaunch; + args: string[]; + cwd?: string; +} +export function probe(options: ProbeOptions): Promise { + return new Promise((resolve, reject) => { + const child = spawn(options.launch.command, [...options.launch.args, ...options.args], { + env: options.launch.env, + cwd: options.cwd, + stdio: "pipe", + detached: process.platform !== "win32", + }); + let stdout = ""; + let stderr = ""; + const deadline = setTimeout(() => { + const termination = Promise.resolve(signalGroup(child, "SIGKILL")); + void termination.then(() => { + reject(new AntigravityError("Antigravity discovery timed out", "PROBE_TIMEOUT")); + return undefined; + }, reject); + }, 10_000); + child.stdout.on("data", (chunk: Buffer) => { + stdout += chunk.toString(); + }); + child.stderr.on("data", (chunk: Buffer) => { + stderr = (stderr + chunk.toString()).slice(-8192); + }); + child.on("error", (error: Error) => { + clearTimeout(deadline); + reject(new AntigravityError(error.message, "SPAWN_ERROR")); + }); + child.on("close", (code) => { + clearTimeout(deadline); + if (code === 0) resolve(stdout); + else + reject( + new AntigravityError( + diagnostic(stderr.trim() || `agy exited with code ${code}`), + "PROBE_ERROR", + ), + ); + }); + }); +} diff --git a/plugins/antigravity-provider/server/internal/prompt.ts b/plugins/antigravity-provider/server/internal/prompt.ts new file mode 100644 index 0000000000..07f4298b35 --- /dev/null +++ b/plugins/antigravity-provider/server/internal/prompt.ts @@ -0,0 +1,64 @@ +import { randomUUID } from "node:crypto"; +import { chmod, mkdtemp, rm, writeFile } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import type { ProviderPrompt } from "@getpaseo/plugin/server/provider"; +import { AntigravityError } from "./wire.js"; + +interface EncodedPrompt { + text: string; + nativeText: string; +} + +export class PromptFiles { + private directory: string | null = null; + + async encode(prompt: ProviderPrompt): Promise { + if ( + prompt.input.type !== "message" || + prompt.delivery === "steer" || + prompt.outputSchema !== undefined + ) + throw new AntigravityError("Antigravity supports messages only"); + const parts = prompt.input.content.map((part) => { + if (part.type === "text") return part; + if (part.type === "image") return { ...part, extension: imageExtension(part.mimeType) }; + throw new AntigravityError("Antigravity supports text and image content only"); + }); + const text: string[] = []; + const native: string[] = []; + for (const part of parts) { + if (part.type === "text") { + text.push(part.text); + native.push(part.text); + } else if (part.type === "image") { + const extension = part.extension; + if (this.directory === null) { + this.directory = await mkdtemp(path.join(os.tmpdir(), "paseo-antigravity-images-")); + await chmod(this.directory, 0o700); + } + const file = path.join(this.directory, `${randomUUID()}.${extension}`); + await writeFile(file, Buffer.from(part.data, "base64"), { flag: "wx", mode: 0o600 }); + await chmod(file, 0o600); + native.push(`Attached image: ${file}`); + } + } + return { text: text.join("\n"), nativeText: native.join("\n") }; + } + + async close(): Promise { + if (this.directory !== null) { + await rm(this.directory, { recursive: true, force: true }); + this.directory = null; + } + } +} + +function imageExtension(mimeType: string): string { + const match = /^image\/([a-z0-9][a-z0-9.+-]*)$/i.exec(mimeType); + if (!match) throw new AntigravityError(`Expected an image MIME type, received ${mimeType}`); + const subtype = match[1].toLowerCase(); + if (subtype === "jpeg") return "jpg"; + if (subtype === "svg+xml") return "svg"; + return subtype; +} diff --git a/plugins/antigravity-provider/server/internal/session.ts b/plugins/antigravity-provider/server/internal/session.ts new file mode 100644 index 0000000000..7121b516b5 --- /dev/null +++ b/plugins/antigravity-provider/server/internal/session.ts @@ -0,0 +1,382 @@ +import { randomUUID } from "node:crypto"; +import { z } from "zod"; +import type { + ProviderCatalog, + ProviderConfigChanges, + ProviderEvent, + ProviderPersistence, + ProviderPrompt, + ProviderSessionConfig, + ProviderLaunch, + ProviderTimelineItem, + ProviderUsage, +} from "@getpaseo/plugin/server/provider"; +import { startDriver, selection, type Driver } from "./process.js"; +import { validateSelection } from "./catalog.js"; +import { PromptFiles } from "./prompt.js"; +import { toolItem } from "./timeline.js"; +import { AntigravityError, diagnostic, type Frame, type Step } from "./wire.js"; + +const persistenceSchema = z + .object({ version: z.literal(1), data: z.object({ conversationId: z.string().min(1) }).strict() }) + .strict(); +interface Turn { + id: string; + text: Map; + usageSteps: Set; + tools: Map>; + usage: Required>; +} +type State = + | { type: "dormant" } + | { type: "starting"; driver: Driver } + | { type: "idle"; driver: Driver } + | { type: "running"; driver: Driver; turn: Turn } + | { type: "stopping"; driver: Driver } + | { type: "closed" }; +interface SessionOptions { + id: string; + config: ProviderSessionConfig; + launch: ProviderLaunch; + catalog(): ProviderCatalog; + persistence?: ProviderPersistence; + emit(event: ProviderEvent): void; +} + +export class Session { + private state: State = { type: "dormant" }; + private config: ProviderSessionConfig; + private conversationId: string | null; + private firstMessage: boolean; + private readonly promptFiles = new PromptFiles(); + private readonly reportedDenials = new Map(); + + constructor(private readonly options: SessionOptions) { + this.config = options.config; + this.conversationId = options.persistence + ? persistenceSchema.parse(options.persistence).data.conversationId + : null; + this.firstMessage = this.conversationId === null; + validateSelection(this.config, options.catalog()); + } + + async open(requestId: string, capabilities: readonly string[]): Promise { + await this.ensureDriver(); + this.emit({ + type: "session.opened", + requestId, + sessionId: this.options.id, + capabilities, + restoration: "core", + persistence: this.config.persist ? this.persistence() : undefined, + cwd: this.config.cwd, + title: this.config.title, + }); + this.publishConfig(); + this.emit({ type: "session.commands", sessionId: this.options.id, commands: [] }); + this.emit({ type: "session.ready", requestId, sessionId: this.options.id }); + } + + async prompt(prompt: ProviderPrompt): Promise { + if (this.state.type === "running") + throw new AntigravityError( + "Antigravity already has an active turn; wait for it to finish", + "TURN_ACTIVE", + ); + const { text, nativeText } = await this.promptFiles.encode(prompt); + await this.ensureDriver(); + if (this.state.type !== "idle") throw new AntigravityError("Antigravity session is not ready"); + const driver = this.state.driver; + const turn: Turn = { + id: randomUUID(), + text: new Map(), + usageSteps: new Set(), + tools: new Map(), + usage: { inputTokens: 0, outputTokens: 0, cachedInputTokens: 0 }, + }; + this.state = { type: "running", driver, turn }; + this.emit({ + type: "session.prompt_result", + sessionId: this.options.id, + clientMessageId: prompt.clientMessageId, + result: { type: "turn", turnId: turn.id }, + }); + this.emit({ + type: "session.turn", + sessionId: this.options.id, + turnId: turn.id, + state: "started", + }); + this.item({ + type: "user_message", + id: prompt.clientMessageId, + clientMessageId: prompt.clientMessageId, + text, + }); + const driverText = + this.firstMessage && this.config.systemPrompt + ? `${this.config.systemPrompt}\n\n${nativeText}` + : nativeText; + try { + await driver.prompt(driverText); + this.firstMessage = false; + } catch (error) { + if (!(error instanceof Error)) throw error; + this.failTurn(new AntigravityError(error.message)); + await driver.stop("interrupt"); + } + } + + configure(changes: ProviderConfigChanges): void { + const config = { + ...this.config, + model: changes.model === null ? undefined : (changes.model ?? this.config.model), + mode: changes.mode === null ? undefined : (changes.mode ?? this.config.mode), + thinkingOption: + changes.thinkingOption === null + ? undefined + : (changes.thinkingOption ?? this.config.thinkingOption), + }; + if (changes.settings && Object.keys(changes.settings).length > 0) + throw new AntigravityError("Antigravity has no configurable settings"); + validateSelection(config, this.options.catalog()); + this.config = config; + this.publishConfig(); + } + + async interrupt(): Promise { + if (this.state.type !== "running") return; + const { driver, turn } = this.state; + this.state = { type: "stopping", driver }; + await driver.stop("interrupt"); + this.finishTools(turn, "canceled"); + this.emit({ + type: "session.turn", + sessionId: this.options.id, + turnId: turn.id, + state: "canceled", + }); + this.state = { type: "dormant" }; + } + + async close(): Promise { + const state = this.state; + this.state = { type: "closed" }; + try { + if (state.type !== "dormant" && state.type !== "closed") await state.driver.stop("close"); + } finally { + await this.promptFiles.close(); + } + if (state.type === "running") { + this.finishTools(state.turn, "canceled"); + this.emit({ + type: "session.turn", + sessionId: this.options.id, + turnId: state.turn.id, + state: "canceled", + }); + } + } + + private async ensureDriver(): Promise { + if (this.state.type === "closed") throw new AntigravityError("Antigravity session is closed"); + if (this.state.type === "stopping") { + await this.state.driver.stop("interrupt"); + this.state = { type: "dormant" }; + } + if (this.state.type === "idle" && this.state.driver.selection !== selection(this.config)) { + const driver = this.state.driver; + this.state = { type: "stopping", driver }; + await driver.stop("close"); + this.state = { type: "dormant" }; + } + if (this.state.type !== "dormant") return; + this.reportedDenials.clear(); + const driver = startDriver({ + launch: this.options.launch, + config: this.config, + conversationId: this.conversationId, + onFrame: (frame) => this.accept(frame), + onExit: (error) => this.failTurn(error), + }); + this.state = { type: "starting", driver }; + try { + const init = await driver.ready; + if (this.conversationId !== null && init.conversation_id !== this.conversationId) + throw new AntigravityError( + "Antigravity restored a different conversation", + "INVALID_RESUME", + ); + this.conversationId = init.conversation_id; + this.state = { type: "idle", driver }; + if (this.config.persist) + this.emit({ + type: "session.persistence", + sessionId: this.options.id, + persistence: this.persistence(), + }); + } catch (error) { + this.state = { type: "dormant" }; + await driver.stop("interrupt"); + throw error; + } + } + + private accept(frame: Frame): void { + if (this.state.type !== "running") return; + const { turn, driver } = this.state; + if (frame.event === "step_update") { + this.acceptStep(frame.step_update, turn); + return; + } + if (frame.event !== "result") return; + const result = frame.result; + const denied = takeNewDenials(result.denied_actions, this.reportedDenials); + if (denied.length > 0) { + this.emit({ + type: "session.notice", + sessionId: this.options.id, + notice: { + id: `${turn.id}:denied`, + severity: "warning", + title: denialNoticeTitle(denied), + description: "Switch to Full access to allow shell commands.", + }, + }); + } + if (result.status === "ERROR") { + const canceled = result.error === "interrupted"; + this.finishTools(turn, canceled ? "canceled" : "failed"); + this.retire(driver); + this.emit({ + type: "session.turn", + sessionId: this.options.id, + turnId: turn.id, + state: canceled ? "canceled" : "failed", + error: { message: diagnostic(result.error) }, + }); + } else { + this.finishTools(turn, "completed"); + this.state = { type: "idle", driver }; + this.emit({ + type: "session.turn", + sessionId: this.options.id, + turnId: turn.id, + state: "completed", + }); + } + } + + private acceptStep(step: Step, turn: Turn): void { + const id = `${turn.id}:${step.step_index}`; + if (step.step_type === "agent_response") { + const previous = turn.text.get(step.step_index) || ""; + const text = previous + step.text_delta; + turn.text.set(step.step_index, text); + if (step.text_delta) this.item({ type: "assistant_message", id, messageId: id, text }); + if (step.state === "DONE" && step.usage && !turn.usageSteps.has(step.step_index)) { + turn.usageSteps.add(step.step_index); + turn.usage.inputTokens += step.usage.input_tokens; + turn.usage.outputTokens += step.usage.output_tokens; + turn.usage.cachedInputTokens += step.usage.cache_read_tokens; + this.emit({ + type: "session.usage", + sessionId: this.options.id, + turnId: turn.id, + usage: { ...turn.usage }, + }); + } + } else if (step.step_type === "tool" || step.step_type === "subagent") { + const item = toolItem(step, id); + turn.tools.set(id, item); + this.item(item); + } + } + + private failTurn(error: AntigravityError): void { + const state = this.state; + if (state.type !== "running" && state.type !== "idle") return; + this.retire(state.driver); + if (state.type === "running") { + this.finishTools(state.turn, "failed"); + this.emit({ + type: "session.turn", + sessionId: this.options.id, + turnId: state.turn.id, + state: "failed", + error: { message: error.message, code: error.code }, + }); + } else + this.emit({ + type: "session.runtime_failed", + sessionId: this.options.id, + error: { message: error.message, code: error.code }, + }); + } + + private retire(driver: Driver): void { + this.state = { type: "stopping", driver }; + void driver.stop("interrupt").then(() => { + if (this.state.type === "stopping" && this.state.driver === driver) + this.state = { type: "dormant" }; + return undefined; + }); + } + + private finishTools(turn: Turn, status: "completed" | "failed" | "canceled"): void { + for (const item of turn.tools.values()) { + if (item.status !== "running") continue; + if (status === "failed") this.item({ ...item, status, error: "Antigravity turn failed" }); + else this.item({ ...item, status, error: null }); + } + } + + private persistence(): ProviderPersistence { + if (this.conversationId === null) throw new AntigravityError("Antigravity has no conversation"); + return { version: 1, data: { conversationId: this.conversationId } }; + } + + private publishConfig(): void { + this.emit({ + type: "session.config", + sessionId: this.options.id, + config: { + model: this.config.model, + mode: this.config.mode || "default", + models: this.options.catalog().models, + modes: this.options.catalog().modes, + thinkingOptions: [], + settings: [], + }, + }); + } + private item(item: ProviderTimelineItem): void { + this.emit({ type: "timeline.item", sessionId: this.options.id, item }); + } + private emit(event: ProviderEvent): void { + this.options.emit(event); + } +} + +// Native results contain every denial since this driver started, including repeated actions. +function takeNewDenials( + actions: Extract["result"]["denied_actions"], + reported: Map, +) { + const counts = new Map(); + const fresh = actions.filter((action) => { + const key = JSON.stringify([action.action, action.display_name]); + const count = (counts.get(key) ?? 0) + 1; + counts.set(key, count); + return count > (reported.get(key) ?? 0); + }); + for (const [key, count] of counts) reported.set(key, Math.max(count, reported.get(key) ?? 0)); + return fresh; +} + +function denialNoticeTitle( + actions: Extract["result"]["denied_actions"], +): string { + if (actions.length > 1) return `${actions.length} actions denied`; + return actions[0].action === "command" ? "Shell command denied" : "Action denied"; +} diff --git a/plugins/antigravity-provider/server/internal/signals.ts b/plugins/antigravity-provider/server/internal/signals.ts new file mode 100644 index 0000000000..be34ecc8c9 --- /dev/null +++ b/plugins/antigravity-provider/server/internal/signals.ts @@ -0,0 +1,40 @@ +import { execFile } from "node:child_process"; +import { promisify } from "node:util"; + +interface SignalOptions { + platform: NodeJS.Platform; + pid: number; + signal: NodeJS.Signals; +} + +type SignalPlan = + | { type: "group"; pid: number; signal: NodeJS.Signals } + | { type: "tree"; command: "taskkill"; args: string[] }; + +const executeFile = promisify(execFile); + +export function signalPlan(options: SignalOptions): SignalPlan { + if (options.platform === "win32") { + return { type: "tree", command: "taskkill", args: ["/PID", String(options.pid), "/T", "/F"] }; + } + return { type: "group", pid: -options.pid, signal: options.signal }; +} + +export function signalProcess(options: SignalOptions): void | Promise { + const plan = signalPlan(options); + if (plan.type === "tree") return killTree(plan); + try { + process.kill(plan.pid, plan.signal); + } catch (error) { + if (!(error instanceof Error && "code" in error && error.code === "ESRCH")) throw error; + } +} + +async function killTree(plan: Extract): Promise { + try { + await executeFile(plan.command, plan.args, { windowsHide: true }); + } catch (error) { + // taskkill reports an already-exited PID with exit code 128. + if (!(error instanceof Error && "code" in error && error.code === 128)) throw error; + } +} diff --git a/plugins/antigravity-provider/server/internal/timeline.ts b/plugins/antigravity-provider/server/internal/timeline.ts new file mode 100644 index 0000000000..22cc81c643 --- /dev/null +++ b/plugins/antigravity-provider/server/internal/timeline.ts @@ -0,0 +1,55 @@ +import type { + ProviderTimelineItem, + ProviderToolCallDetail, +} from "@getpaseo/plugin/server/provider"; +import type { Step } from "./wire.js"; + +export function toolItem( + step: Step, + id: string, +): Extract { + const detail = toolDetail(step); + const name = step.tool_name || "Antigravity tool"; + const metadata = step.subagent_info ? { subagents: step.subagent_info.subagents } : undefined; + const base = { type: "tool_call" as const, id, callId: id, name, detail, metadata }; + if (step.state === "ERROR") { + const error = + step.tool_info && step.tool_info.error + ? step.tool_info.error.message + : "Antigravity tool failed"; + return { ...base, status: "failed", error }; + } + const status = step.state === "ACTIVE" ? "running" : "completed"; + return { ...base, status, error: null }; +} + +function toolDetail(step: Step): ProviderToolCallDetail { + if (step.subagent_info) { + const child = step.subagent_info.subagents[0]; + if (child) + return { + type: "sub_agent", + subAgentType: child.type_name, + description: child.initial_prompt, + childSessionId: child.conversation_id === null ? undefined : child.conversation_id, + log: child.log_uri, + }; + } + const info = step.tool_info; + if (!info) return { type: "plain_text", label: step.tool_name, text: "" }; + const path = info.parameters.AbsolutePath || info.parameters.TargetFile; + if (info.name === "run_command" && typeof info.parameters.CommandLine === "string") { + return { type: "shell", command: info.parameters.CommandLine, output: info.output }; + } + if (info.name === "view_file" && typeof path === "string") + return { type: "read", filePath: path, content: info.output }; + if (info.name === "write_to_file" && typeof path === "string") + return { type: "write", filePath: path }; + if ( + (info.name === "replace_file_content" || info.name === "multi_replace_file_content") && + typeof path === "string" + ) + return { type: "edit", filePath: path }; + // Native frames summarize writes; they do not carry the file contents or diff. + return { type: "unknown", input: info.parameters, output: info.error || info.output }; +} diff --git a/plugins/antigravity-provider/server/internal/wire.ts b/plugins/antigravity-provider/server/internal/wire.ts new file mode 100644 index 0000000000..eef417c83c --- /dev/null +++ b/plugins/antigravity-provider/server/internal/wire.ts @@ -0,0 +1,85 @@ +import { z } from "zod"; + +const usage = z.object({ + input_tokens: z.number().nonnegative(), + output_tokens: z.number().nonnegative(), + cache_read_tokens: z.number().nonnegative().default(0), +}); +const tool = z.object({ + name: z.string(), + parameters: z.record(z.string(), z.json()).default({}), + output: z.string().default(""), + error: z.object({ type: z.string(), message: z.string() }).nullable().default(null), +}); +const child = z.object({ + type_name: z.string(), + role: z.string().default(""), + initial_prompt: z.string().default(""), + conversation_id: z.string().nullable().default(null), + log_uri: z.string().default(""), + workspace_uris: z.array(z.string()).default([]), +}); +const frame = z.discriminatedUnion("event", [ + z.object({ + event: z.literal("init"), + conversation_id: z.string().min(1), + init: z.object({ model: z.string().nullable().default(null) }), + }), + z.object({ + event: z.literal("step_update"), + step_update: z.object({ + conversation_id: z.string(), + step_index: z.number().int().nonnegative(), + state: z.enum(["ACTIVE", "DONE", "ERROR"]), + step_type: z.string(), + text_delta: z.string().default(""), + tool_name: z.string().default(""), + tool_info: tool.nullable().default(null), + subagent_info: z + .object({ subagents: z.array(child) }) + .nullable() + .default(null), + usage: usage.nullable().default(null), + }), + }), + z.object({ + event: z.literal("result"), + result: z.object({ + conversation_id: z.string(), + status: z.enum(["SUCCESS", "ERROR"]), + response: z.string().default(""), + error: z.string().default("Antigravity turn failed"), + denied_actions: z + .array(z.object({ action: z.string(), display_name: z.string() })) + .default([]), + }), + }), +]); +export type Frame = z.infer; +export type Step = Extract["step_update"]; +export type Init = Extract; + +export function decodeFrame(line: string): Frame { + return frame.parse(JSON.parse(line)); +} + +export function encodePrompt(text: string): string { + return `${JSON.stringify({ event: "user", message: { content: text } })}\n`; +} + +export class AntigravityError extends Error { + constructor( + message: string, + readonly code = "ANTIGRAVITY_ERROR", + ) { + super(message); + this.name = "AntigravityError"; + } +} + +export function diagnostic(message: string): string { + if (/auth|sign.in|log.in/i.test(message)) { + return "Antigravity authentication failed. Run `agy` and sign in, then retry."; + } + return message; +} diff --git a/plugins/antigravity-provider/server/provider.test.ts b/plugins/antigravity-provider/server/provider.test.ts new file mode 100644 index 0000000000..b9a478c7c6 --- /dev/null +++ b/plugins/antigravity-provider/server/provider.test.ts @@ -0,0 +1,717 @@ +import { mkdtemp, readFile, rm, stat } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { afterEach, describe, expect, it } from "vitest"; +import { + PROVIDER_CAPABILITIES, + ProviderEventSchema, + type ProviderConnection, + type ProviderContent, + type ProviderEvent, + type ProviderInput, + type ProviderLaunch, + type ProviderPersistence, +} from "@getpaseo/plugin/server/provider"; +import { signalPlan } from "./internal/signals.js"; +import { createAntigravityProvider } from "./provider.js"; + +const temporary: string[] = []; +const connections: ProviderConnection[] = []; +const fake = fileURLToPath(new URL("../test/agy.mjs", import.meta.url)); +const provider = createAntigravityProvider(); + +it("publishes a monochrome, theme-aware provider icon", async () => { + const icon = await readFile(new URL("../icon.svg", import.meta.url), "utf8"); + expect(icon).toContain('viewBox="0 0 24 24"'); + expect(icon).toContain('fill="currentColor"'); + expect(icon).not.toMatch(/#[\da-f]{3,8}\b|rgba?\(|hsla?\(|url\(/i); + expect(icon).not.toMatch(/<(?:style|mask|defs|linearGradient|radialGradient)\b|\bid=/i); + expect([...icon.matchAll(/\b(?:fill|stroke)="([^"]+)"/g)].map((match) => match[1])).toEqual([ + "currentColor", + ]); +}); + +afterEach(async () => { + await Promise.all(connections.splice(0).map((connection) => connection.close())); + await Promise.all( + temporary.splice(0).map((directory) => rm(directory, { recursive: true, force: true })), + ); +}); + +async function harness(env: Record = {}) { + const cwd = await mkdtemp(path.join(os.tmpdir(), "antigravity-test-")); + temporary.push(cwd); + const log = path.join(cwd, "argv.ndjson"); + const launch: ProviderLaunch = { + command: process.execPath, + args: [fake, "--launch-prefix"], + env: { AGY_TEST_LOG: log, ...env }, + }; + const connection = await provider.connect({ + versions: [1], + capabilities: PROVIDER_CAPABILITIES, + launch, + }); + connections.push(connection); + const events: ProviderEvent[] = []; + connection.onEvent((event) => { + ProviderEventSchema.parse(event); + events.push(event); + }); + async function wait(predicate: (event: ProviderEvent) => boolean) { + await expect.poll(() => events.some(predicate), { timeout: 5000, interval: 10 }).toBe(true); + const event = events.find(predicate); + if (!event) throw new Error("Expected event missing"); + return event; + } + async function request(input: ProviderInput) { + await connection.send(input); + if ("requestId" in input) + await wait( + (event) => + "requestId" in event && + event.requestId === input.requestId && + (event.type === "request.completed" || + event.type === "request.failed" || + event.type === "session.ready" || + event.type === "catalog"), + ); + } + async function open(restored?: ProviderPersistence, sessionId = "s") { + await request({ + type: "session.open", + requestId: `open-${events.length}`, + sessionId, + persistence: restored, + history: "replay", + config: { + cwd, + env: { AGY_SESSION_VALUE: "session-overlay" }, + settings: {}, + mcpServers: {}, + persist: true, + systemPrompt: "SYSTEM PREFIX", + }, + }); + } + async function message(content: ProviderContent[], sessionId = "s") { + const clientMessageId = `prompt-${events.length}`; + await connection.send({ + type: "session.prompt", + sessionId, + prompt: { + clientMessageId, + delivery: "auto", + input: { type: "message", content }, + }, + }); + await wait( + (event) => + event.type === "session.prompt_result" && event.clientMessageId === clientMessageId, + ); + return clientMessageId; + } + function prompt(text: string) { + return message([{ type: "text", text }]); + } + async function completed(clientMessageId: string) { + const result = events.find( + (event) => + event.type === "session.prompt_result" && event.clientMessageId === clientMessageId, + ); + if (!result || result.type !== "session.prompt_result" || result.result.type !== "turn") + throw new Error("Prompt was not admitted"); + const turnId = result.result.turnId; + return wait( + (event) => + event.type === "session.turn" && event.turnId === turnId && event.state !== "started", + ); + } + async function records() { + return (await readFile(log, "utf8")) + .trim() + .split("\n") + .map((line) => JSON.parse(line)); + } + function persistence() { + const opened = events.find((event) => event.type === "session.opened"); + if (!opened || opened.type !== "session.opened" || !opened.persistence) + throw new Error("Missing persistence"); + return opened.persistence; + } + return { + connection, + events, + launch, + cwd, + wait, + request, + open, + prompt, + message, + completed, + records, + persistence, + }; +} + +it("streams complete text snapshots, prefixes the system prompt only once and preserves launch argv/env", async () => { + const h = await harness(); + expect(h.connection.capabilities).toEqual([ + "prompt.message", + "prompt.image", + "session.configure", + "session.persistence", + ]); + await h.open(); + await h.completed(await h.prompt("HELLO")); + await h.completed(await h.prompt("HELLO AGAIN")); + const text = h.events.filter( + (event) => event.type === "timeline.item" && event.item.type === "assistant_message", + ); + expect( + text.map((event) => + event.type === "timeline.item" && event.item.type === "assistant_message" + ? event.item.text + : "", + ), + ).toEqual(["PHASE0_HELLO", "PHASE0_HELLO\n", "PHASE0_HELLO", "PHASE0_HELLO\n"]); + const records = await h.records(); + expect(records.find((entry) => entry.args.includes("--input-format")).args).toEqual([ + "--launch-prefix", + "--input-format", + "stream-json", + "--output-format", + "stream-json", + "--add-dir", + h.cwd, + "--disable-slash-commands", + ]); + expect(records.find((entry) => entry.args.includes("--input-format")).env).toBe( + "session-overlay", + ); + expect( + records.filter((entry) => entry.input).map((entry) => entry.input.message.content), + ).toEqual(["SYSTEM PREFIX\n\nHELLO", "HELLO AGAIN"]); +}); + +it("maps tools and emits one denial notice for a SUCCESS result", async () => { + const h = await harness(); + await h.open(); + // This capture contains three distinct native results, one for each submitted prompt. + await h.completed(await h.prompt("TOOLS")); + await h.completed(await h.prompt("TOOLS")); + const result = await h.completed(await h.prompt("TOOLS")); + expect(result).toMatchObject({ type: "session.turn", state: "completed" }); + const tools = h.events.filter( + (event) => event.type === "timeline.item" && event.item.type === "tool_call", + ); + expect(tools).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + item: expect.objectContaining({ + name: "view_file", + status: "completed", + detail: expect.objectContaining({ type: "read" }), + }), + }), + expect.objectContaining({ + item: expect.objectContaining({ + name: "write_to_file", + status: "completed", + detail: expect.objectContaining({ type: "write" }), + }), + }), + expect.objectContaining({ + item: expect.objectContaining({ + name: "run_command", + status: "failed", + detail: expect.objectContaining({ type: "shell", command: "printf PHASE0_SHELL" }), + }), + }), + ]), + ); + const notices = h.events.filter((event) => event.type === "session.notice"); + expect(notices).toHaveLength(1); + expect(notices[0]).toMatchObject({ + notice: { + title: "Shell command denied", + description: "Switch to Full access to allow shell commands.", + }, + }); +}); + +it("reports only new process denials across replayed turns and resets after respawn", async () => { + const h = await harness(); + await h.open(); + await h.completed(await h.prompt("DENIAL_REPLAY")); + expect(h.events.filter((event) => event.type === "session.notice")).toHaveLength(1); + const followupStart = h.events.length; + await h.completed(await h.prompt("DENIAL_REPLAY")); + const followup = h.events.slice(followupStart); + expect(followup.some((event) => event.type === "session.notice")).toBe(false); + expect(followup).toContainEqual( + expect.objectContaining({ + type: "timeline.item", + item: expect.objectContaining({ type: "assistant_message", text: "FOLLOWUP_OK\n" }), + }), + ); + await h.request({ + type: "session.configure", + sessionId: "s", + requestId: "mode", + changes: { mode: "plan" }, + }); + await h.completed(await h.prompt("DENIAL_REPLAY")); + expect(h.events.filter((event) => event.type === "session.notice")).toHaveLength(2); + expect( + (await h.records()).filter((entry) => entry.args?.includes("--input-format")), + ).toHaveLength(2); +}); + +it("counts repeated native action denials without exposing tool identifiers", async () => { + const h = await harness({ AGY_TEST_DENIAL_COUNTS: "2,2,3" }); + await h.open(); + await h.completed(await h.prompt("DENIAL_REPLAY")); + await h.completed(await h.prompt("DENIAL_REPLAY")); + const notices = h.events.filter((event) => event.type === "session.notice"); + expect(notices).toHaveLength(1); + expect(notices[0]).toMatchObject({ + notice: { + title: "2 actions denied", + description: "Switch to Full access to allow shell commands.", + }, + }); + await h.completed(await h.prompt("DENIAL_REPLAY")); + expect(h.events.filter((event) => event.type === "session.notice")).toHaveLength(2); + expect(h.events.findLast((event) => event.type === "session.notice")).toMatchObject({ + notice: { title: "Shell command denied" }, + }); +}); + +it("accounts DONE generation usage once without adding cumulative results, including resume", async () => { + const h = await harness(); + await h.open(); + await h.completed(await h.prompt("MULTI")); + await h.completed(await h.prompt("MULTI")); + const usages = h.events.filter((event) => event.type === "session.usage"); + expect( + usages.map((event) => (event.type === "session.usage" ? event.usage.inputTokens : null)), + ).toEqual([11263, 11604]); + const persistence = h.persistence(); + await h.request({ type: "session.close", sessionId: "s", requestId: "close" }); + await h.open(persistence); + await h.completed(await h.prompt("RESUME")); + expect(h.events.filter((event) => event.type === "session.usage")).toHaveLength(3); + expect(h.events.findLast((event) => event.type === "session.usage")).toMatchObject({ + usage: { inputTokens: 11815 }, + }); +}); + +it("maps subagent steps to a tool carrying the child conversation ID", async () => { + const h = await harness(); + await h.open(); + await h.completed(await h.prompt("SUBAGENT")); + expect(h.events).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + type: "timeline.item", + item: expect.objectContaining({ + name: "invoke_subagent", + status: "completed", + detail: expect.objectContaining({ + type: "sub_agent", + childSessionId: "b6f02329-7415-49a7-a9a9-1cd09e898d26", + }), + }), + }), + ]), + ); +}); + +it.each(["HANG", "TOOL_HANG"])( + "interrupts %s, settles canceled, and respawns the same conversation", + async (text) => { + const h = await harness(); + await h.open(); + const message = await h.prompt(text); + await h.wait( + (event) => + event.type === "timeline.item" && + (event.item.type === "assistant_message" || event.item.type === "tool_call"), + ); + await h.request({ type: "session.interrupt", requestId: "interrupt", sessionId: "s" }); + expect(await h.completed(message)).toMatchObject({ state: "canceled" }); + await h.completed(await h.prompt("HELLO")); + const records = await h.records(); + const launches = records.filter((entry) => entry.args && entry.args.includes("--input-format")); + expect(launches).toHaveLength(2); + expect(launches[1].args.slice(-2)).toEqual([ + "--conversation", + "e5cf1e2d-c715-4328-8e67-4a85c8dc3cda", + ]); + // Windows taskkill terminates the tree without invoking the CLI's signal handlers. + expect(records.filter((entry) => entry.signal)).toEqual( + process.platform === "win32" ? [] : [{ signal: "SIGINT" }], + ); + const canceled = h.events.filter( + (event) => event.type === "session.turn" && event.state === "canceled", + ); + expect(canceled).toHaveLength(1); + }, +); + +it("stops an unresponsive driver and still respawns the conversation", async () => { + const h = await harness({ AGY_TEST_IGNORE_SIGNALS: "yes" }); + await h.open(); + const message = await h.prompt("HANG"); + await h.wait( + (event) => event.type === "timeline.item" && event.item.type === "assistant_message", + ); + await h.request({ type: "session.interrupt", requestId: "interrupt-timeout", sessionId: "s" }); + expect(await h.completed(message)).toMatchObject({ state: "canceled" }); + expect((await h.records()).filter((entry) => entry.signal)).toEqual( + process.platform === "win32" ? [] : [{ signal: "SIGINT" }, { signal: "SIGTERM" }], + ); + await h.completed(await h.prompt("HELLO")); +}); + +it.each(["accept-edits", "plan"])( + "launches native %s mode without claiming read-only access", + async (mode) => { + const h = await harness(); + await h.open(); + await h.request({ + type: "session.configure", + requestId: "mode", + sessionId: "s", + changes: { mode }, + }); + await h.completed(await h.prompt("HELLO")); + const launches = (await h.records()).filter( + (entry) => entry.args && entry.args.includes("--input-format"), + ); + expect(launches[1].args).toContain("--mode"); + expect(launches[1].args[launches[1].args.indexOf("--mode") + 1]).toBe(mode); + }, +); + +it("defers model and mode changes until the next prompt, then respawns with validated flags", async () => { + const h = await harness(); + await h.open(); + await h.request({ + type: "session.configure", + requestId: "change", + sessionId: "s", + changes: { model: "gemini-3.8-flash-low", mode: "full-access" }, + }); + expect( + (await h.records()).filter((entry) => entry.args && entry.args.includes("--input-format")), + ).toHaveLength(1); + await h.completed(await h.prompt("HELLO")); + const launches = (await h.records()).filter( + (entry) => entry.args && entry.args.includes("--input-format"), + ); + expect(launches).toHaveLength(2); + expect(launches[1].args.slice(-5)).toEqual([ + "--model", + "gemini-3.8-flash-low", + "--dangerously-skip-permissions", + "--conversation", + "e5cf1e2d-c715-4328-8e67-4a85c8dc3cda", + ]); + await h.request({ + type: "session.configure", + requestId: "invalid", + sessionId: "s", + changes: { model: "unknown" }, + }); + expect(h.events).toContainEqual( + expect.objectContaining({ + type: "request.failed", + requestId: "invalid", + error: expect.objectContaining({ code: "INVALID_MODEL" }), + }), + ); +}); + +it("resumes after connection close without replay or another system prefix", async () => { + const first = await harness(); + await first.open(); + await first.completed(await first.prompt("HELLO")); + const persistence = first.persistence(); + await first.connection.close(); + const second = await harness(); + await second.open(persistence); + expect(second.events.filter((event) => event.type === "timeline.item")).toEqual([]); + await second.completed(await second.prompt("RESUME")); + expect( + (await second.records()) + .filter((entry) => entry.input) + .map((entry) => entry.input.message.content), + ).toEqual(["RESUME"]); +}); + +it.each(["auth", "exit", "stderr"])( + "reports startup %s without waiting for init forever", + async (startup) => { + const h = await harness({ AGY_TEST_STARTUP: startup }); + await h.open(); + expect(h.events.filter((event) => event.type === "session.ready")).toEqual([]); + const failure = h.events.find((event) => event.type === "request.failed"); + expect(failure).toMatchObject({ + type: "request.failed", + error: { + message: expect.stringMatching(startup === "exit" ? /exited/ : /Run `agy` and sign in/), + }, + }); + }, +); + +it("rejects mid-turn steering and reports a busy message without queueing a second native turn", async () => { + const h = await harness(); + await h.open(); + await h.prompt("HANG"); + const second = await h.prompt("SECOND"); + expect( + h.events.find( + (event) => event.type === "session.prompt_result" && event.clientMessageId === second, + ), + ).toMatchObject({ + type: "session.prompt_result", + result: { type: "failed", error: { code: "TURN_ACTIVE" } }, + }); + await expect( + h.connection.send({ + type: "session.prompt", + sessionId: "s", + prompt: { + clientMessageId: "steer", + delivery: "steer", + input: { type: "message", content: [{ type: "text", text: "x" }] }, + }, + }), + ).rejects.toThrow(/prompt.steer/); +}); + +describe("discovery", () => { + it("reuses one discovered catalog for two opens and configure, and refreshes explicit catalog requests", async () => { + const h = await harness(); + await h.open(); + await h.open(undefined, "second"); + await h.request({ + type: "session.configure", + requestId: "configure-cached", + sessionId: "second", + changes: { model: "gemini-3.8-flash-low" }, + }); + expect( + (await h.records()).filter((entry) => entry.args && entry.args.includes("models")), + ).toHaveLength(1); + await h.request({ type: "catalog", requestId: "refresh" }); + expect( + (await h.records()).filter((entry) => entry.args && entry.args.includes("models")), + ).toHaveLength(2); + }); + it.each(["1.1.15", "1.2.12", "1.2.13"])("accepts driver-capable version %s", async (version) => { + const h = await harness({ AGY_TEST_VERSION: version }); + expect(await provider.status?.({ launch: h.launch })).toEqual({ available: true }); + }); + + it("parses tab-separated catalog and exposes native permission choices without thinking options", async () => { + const h = await harness(); + await h.request({ type: "catalog", requestId: "catalog" }); + const event = h.events[0]; + expect(event).toMatchObject({ + type: "catalog", + catalog: { + models: expect.arrayContaining([ + { id: "gemini-3.8-flash-low", label: "Gemini 3.8 Flash (Low)" }, + ]), + thinkingOptions: [], + defaultMode: "default", + }, + }); + if (event.type !== "catalog") throw new Error("Missing catalog"); + expect(event.catalog.models).toHaveLength(11); + expect(event.catalog.modes.map((mode) => mode.label)).toEqual([ + "Default", + "Accept edits", + "Plan", + "Full access", + ]); + expect(event.catalog.modes.map((mode) => mode.id)).toEqual([ + "default", + "accept-edits", + "plan", + "full-access", + ]); + expect( + event.catalog.modes.map(({ id, icon, colorTier, isUnattended }) => ({ + id, + icon, + colorTier, + isUnattended, + })), + ).toEqual([ + { id: "default", icon: "Shield", colorTier: "moderate", isUnattended: undefined }, + { id: "accept-edits", icon: "ShieldPlus", colorTier: "moderate", isUnattended: undefined }, + { id: "plan", icon: "ShieldEllipsis", colorTier: "planning", isUnattended: undefined }, + { id: "full-access", icon: "ShieldOff", colorTier: "dangerous", isUnattended: true }, + ]); + }); + it("reports missing, old, unauthenticated and available launches", async () => { + expect(await provider.status?.({})).toMatchObject({ + available: false, + diagnostic: expect.stringContaining("agy not found"), + }); + const missing = await harness(); + expect( + await provider.status?.({ + launch: { ...missing.launch, command: path.join(missing.cwd, "missing-agy") }, + }), + ).toMatchObject({ available: false, diagnostic: expect.stringContaining("ENOENT") }); + const old = await harness({ AGY_TEST_VERSION: "1.1.14" }); + expect(await provider.status?.({ launch: old.launch })).toMatchObject({ + available: false, + diagnostic: expect.stringContaining("1.1.15"), + }); + const unauthenticated = await harness({ AGY_TEST_AUTH: "missing" }); + expect(await provider.status?.({ launch: unauthenticated.launch })).toEqual({ + available: false, + diagnostic: "Antigravity authentication failed. Run `agy` and sign in, then retry.", + }); + const authenticated = await harness(); + expect(await provider.status?.({ launch: authenticated.launch })).toEqual({ available: true }); + }); + it("keys catalog caching by the complete effective launch without exposing env values", async () => { + const h = await harness(); + const key = await provider.getCatalogCacheKey?.({ scope: "global", launch: h.launch }); + expect(key).toMatch(/^[a-f0-9]{64}$/); + expect( + await provider.getCatalogCacheKey?.({ + scope: "workspace", + cwd: h.cwd, + force: true, + launch: h.launch, + }), + ).toBe(key); + expect( + await provider.getCatalogCacheKey?.({ + scope: "global", + launch: { ...h.launch, args: [...h.launch.args, "changed"] }, + }), + ).not.toBe(key); + expect( + await provider.getCatalogCacheKey?.({ + scope: "global", + launch: { ...h.launch, env: { ...h.launch.env, HOME: "different" } }, + }), + ).not.toBe(key); + }); +}); + +describe("process signal plans", () => { + it.each(["SIGINT", "SIGTERM", "SIGKILL"] as const)("kills the Windows tree for %s", (signal) => { + expect(signalPlan({ platform: "win32", pid: 123, signal })).toEqual({ + type: "tree", + command: "taskkill", + args: ["/PID", "123", "/T", "/F"], + }); + }); + it.each(["SIGINT", "SIGTERM", "SIGKILL"] as const)( + "preserves POSIX group signal %s", + (signal) => { + expect(signalPlan({ platform: "linux", pid: 123, signal })).toEqual({ + type: "group", + pid: -123, + signal, + }); + }, + ); +}); + +describe("image prompts", () => { + it.each(["session", "connection"])( + "encodes mixed content as private session files and cleans up on %s close", + async (close) => { + const h = await harness(); + await h.open(); + const png = await readFile( + new URL("../test/fixtures/image-file-external.png", import.meta.url), + ); + const jpeg = Buffer.from("image file bytes"); + await h.completed( + await h.message([ + { type: "text", text: "Describe these images." }, + { type: "image", data: png.toString("base64"), mimeType: "image/png" }, + { type: "text", text: "Compare them." }, + { type: "image", data: jpeg.toString("base64"), mimeType: "image/jpeg" }, + ]), + ); + const sent = (await h.records()).find((entry) => entry.input).input.message.content; + const lines = sent.split("\n"); + const pngPath = lines[3].slice("Attached image: ".length); + const jpegPath = lines[5].slice("Attached image: ".length); + expect(sent).toBe( + `SYSTEM PREFIX\n\nDescribe these images.\nAttached image: ${pngPath}\nCompare them.\nAttached image: ${jpegPath}`, + ); + expect(path.isAbsolute(pngPath)).toBe(true); + expect(path.extname(pngPath)).toBe(".png"); + expect(path.extname(jpegPath)).toBe(".jpg"); + expect(path.dirname(pngPath)).toBe(path.dirname(jpegPath)); + expect(path.dirname(pngPath)).not.toBe(h.cwd); + expect(await readFile(pngPath)).toEqual(png); + expect(await readFile(jpegPath)).toEqual(jpeg); + if (process.platform !== "win32") { + expect((await stat(path.dirname(pngPath))).mode & 0o777).toBe(0o700); + expect((await stat(pngPath)).mode & 0o777).toBe(0o600); + expect((await stat(jpegPath)).mode & 0o777).toBe(0o600); + } + const args = (await h.records()).find((entry) => entry.args?.includes("--input-format")).args; + expect(args.filter((arg: string) => arg === "--add-dir")).toHaveLength(1); + if (close === "connection") await h.connection.close(); + else await h.request({ type: "session.close", requestId: "close-images", sessionId: "s" }); + await expect(stat(path.dirname(pngPath))).rejects.toMatchObject({ code: "ENOENT" }); + }, + ); + + it("keeps image-only prompts in distinct session directories until each session closes", async () => { + const h = await harness(); + await h.open(); + await h.open(undefined, "other"); + const content: ProviderContent[] = [ + { type: "image", data: "aW1hZ2U=", mimeType: "image/webp" }, + ]; + await h.completed(await h.message(content)); + await h.completed(await h.message(content)); + await h.completed(await h.message(content, "other")); + const paths = (await h.records()) + .filter((entry) => entry.input) + .map((entry) => entry.input.message.content.split("Attached image: ")[1]); + expect(new Set(paths).size).toBe(3); + expect(path.dirname(paths[0])).toBe(path.dirname(paths[1])); + expect(path.dirname(paths[0])).not.toBe(path.dirname(paths[2])); + expect(paths.every((file: string) => path.extname(file) === ".webp")).toBe(true); + await h.request({ type: "session.close", requestId: "close-first", sessionId: "s" }); + await expect(stat(path.dirname(paths[0]))).rejects.toMatchObject({ code: "ENOENT" }); + expect(await readFile(paths[2], "utf8")).toBe("image"); + await h.connection.close(); + await expect(stat(path.dirname(paths[2]))).rejects.toMatchObject({ code: "ENOENT" }); + }); + + it("rejects a non-image MIME type before writing to native stdin", async () => { + const h = await harness(); + await h.open(); + const id = await h.message([{ type: "image", data: "dGV4dA==", mimeType: "text/plain" }]); + expect( + h.events.find( + (event) => event.type === "session.prompt_result" && event.clientMessageId === id, + ), + ).toMatchObject({ + result: { type: "failed", error: { message: expect.stringContaining("image MIME type") } }, + }); + expect((await h.records()).filter((entry) => entry.input)).toHaveLength(0); + }); +}); diff --git a/plugins/antigravity-provider/server/provider.ts b/plugins/antigravity-provider/server/provider.ts new file mode 100644 index 0000000000..88c73c7faa --- /dev/null +++ b/plugins/antigravity-provider/server/provider.ts @@ -0,0 +1,171 @@ +import { createHash } from "node:crypto"; +import { + negotiateProviderCapabilities, + requireProviderCapabilities, + type ProviderCatalog, + type ProviderConnection, + type ProviderEvent, + type ProviderInput, + type ProviderLaunch, + type ProviderRegistration, +} from "@getpaseo/plugin/server/provider"; +import { getCatalog, getStatus } from "./internal/catalog.js"; +import { Session } from "./internal/session.js"; +import { AntigravityError } from "./internal/wire.js"; + +const capabilities = [ + "prompt.message", + "prompt.image", + "session.configure", + "session.persistence", +] as const; + +export function createAntigravityProvider(): ProviderRegistration { + return { + id: "antigravity", + label: "Antigravity", + icon: "icon.svg", + command: ["agy"], + description: "Antigravity CLI on your host", + async status({ launch }) { + if (!launch) + return { + available: false, + diagnostic: "agy not found on PATH. Install Antigravity, then run `agy` and sign in.", + }; + return getStatus(launch); + }, + async getCatalogCacheKey({ launch }) { + if (!launch) return undefined; + const env = Object.entries(launch.env).sort(([left], [right]) => left.localeCompare(right)); + return createHash("sha256") + .update(JSON.stringify([launch.command, launch.args, env])) + .digest("hex"); + }, + async connect(request) { + if (!request.versions.includes(1)) + throw new AntigravityError("Provider protocol version 1 is required"); + if (!request.launch) + throw new AntigravityError("The daemon must supply the Antigravity launch"); + const negotiated = negotiateProviderCapabilities(request.capabilities, capabilities); + return createConnection(request.launch, negotiated); + }, + }; +} + +function createConnection( + launch: ProviderLaunch, + negotiated: readonly string[], +): ProviderConnection { + const sessions = new Map(); + const listeners = new Set<(event: ProviderEvent) => void>(); + let discovered: ProviderCatalog | null = null; + let closed = false; + let pending = Promise.resolve(); + + function emit(event: ProviderEvent): void { + if (closed) return; + for (const listener of listeners) listener(event); + } + function catalog(): ProviderCatalog { + if (discovered === null) + throw new AntigravityError("Antigravity catalog has not been discovered"); + return discovered; + } + function session(id: string): Session { + const found = sessions.get(id); + if (!found) throw new AntigravityError(`Unknown Antigravity session: ${id}`); + return found; + } + async function dispatch(input: ProviderInput): Promise { + switch (input.type) { + case "catalog": + discovered = await getCatalog(launch, input.cwd); + emit({ + type: "catalog", + requestId: input.requestId, + catalog: catalog(), + }); + return; + case "session.open": { + if (sessions.has(input.sessionId)) + throw new AntigravityError(`Session already exists: ${input.sessionId}`); + if (discovered === null) discovered = await getCatalog(launch, input.config.cwd); + const opened = new Session({ + id: input.sessionId, + config: input.config, + persistence: input.persistence, + catalog, + launch, + emit, + }); + sessions.set(input.sessionId, opened); + try { + await opened.open(input.requestId, negotiated); + } catch (error) { + sessions.delete(input.sessionId); + await opened.close(); + throw error; + } + return; + } + case "session.prompt": + await session(input.sessionId).prompt(input.prompt); + return; + case "session.configure": + session(input.sessionId).configure(input.changes); + break; + case "session.interrupt": + await session(input.sessionId).interrupt(); + break; + case "session.close": { + await session(input.sessionId).close(); + sessions.delete(input.sessionId); + emit({ type: "session.closed", sessionId: input.sessionId }); + break; + } + default: + throw new AntigravityError(`Unsupported Antigravity operation: ${input.type}`); + } + emit({ type: "request.completed", requestId: input.requestId }); + } + function failed(input: ProviderInput, error: unknown): void { + const message = error instanceof Error ? error.message : String(error); + const code = error instanceof AntigravityError ? error.code : "ANTIGRAVITY_ERROR"; + if (input.type === "session.prompt") + emit({ + type: "session.prompt_result", + sessionId: input.sessionId, + clientMessageId: input.prompt.clientMessageId, + result: { type: "failed", error: { message, code } }, + }); + else if ("requestId" in input) + emit({ type: "request.failed", requestId: input.requestId, error: { message, code } }); + } + return { + version: 1, + capabilities: negotiated, + async send(input) { + if (closed) throw new AntigravityError("Antigravity connection is closed"); + requireProviderCapabilities(negotiated, input); + pending = pending + .then(async () => { + if (!closed) await dispatch(input); + return undefined; + }) + .catch((error) => failed(input, error)); + }, + onEvent(listener) { + listeners.add(listener); + return () => listeners.delete(listener); + }, + async close() { + if (closed) return; + closed = true; + await pending; + await Promise.all([...sessions.values()].map((opened) => opened.close())); + sessions.clear(); + listeners.clear(); + }, + }; +} diff --git a/plugins/antigravity-provider/test/agy.mjs b/plugins/antigravity-provider/test/agy.mjs new file mode 100644 index 0000000000..91b0531b56 --- /dev/null +++ b/plugins/antigravity-provider/test/agy.mjs @@ -0,0 +1,102 @@ +import { readFileSync, appendFileSync } from "node:fs"; +import { createInterface } from "node:readline"; + +const args = process.argv.slice(2); +const log = process.env.AGY_TEST_LOG; +function record(value) { + if (log) appendFileSync(log, `${JSON.stringify(value)}\n`); +} +record({ args, pid: process.pid, env: process.env.AGY_SESSION_VALUE }); +function fixture(name, stream = "stdout") { + return readFileSync(new URL(`./fixtures/${name}.${stream}.ndjson`, import.meta.url), "utf8") + .trim() + .split("\n") + .filter(Boolean) + .map((line) => JSON.parse(line).data); +} +if (args.includes("--version")) { + process.stdout.write(`${process.env.AGY_TEST_VERSION || "1.2.13"}\n`); + process.exit(0); +} +if (args.includes("models")) { + if (process.env.AGY_TEST_AUTH === "missing") { + process.stderr.write(fixture("models-unauthenticated", "stderr").join("\n")); + process.exit(1); + } + process.stdout.write(fixture("models-api-key").join("\n") + "\n"); + process.exit(0); +} +if (process.env.AGY_TEST_STARTUP === "auth") { + process.stderr.write(fixture("not-signed-in", "stderr").join("\n")); + process.stdout.write(JSON.stringify(fixture("not-signed-in")[0]) + "\n"); + process.exit(1); +} +if (process.env.AGY_TEST_STARTUP === "exit") process.exit(3); +if (process.env.AGY_TEST_STARTUP === "stderr") { + process.stderr.write("Error: authentication required. Run 'agy' to log in, then retry.\n"); + process.exit(1); +} +const resumeIndex = args.indexOf("--conversation"); +const conversationId = + resumeIndex >= 0 ? args[resumeIndex + 1] : fixture("text-turn")[0].conversation_id; +let denialResults = 0; +function emit(frame) { + const converted = structuredClone(frame); + if (converted.conversation_id !== undefined) converted.conversation_id = conversationId; + if (converted.step_update) converted.step_update.conversation_id = conversationId; + if (converted.result) { + converted.result.conversation_id = conversationId; + if (process.env.AGY_TEST_DENIAL_COUNTS && converted.result.denied_actions?.length) { + converted.result.denied_actions = Array.from( + { length: Number(process.env.AGY_TEST_DENIAL_COUNTS.split(",")[denialResults++]) }, + () => converted.result.denied_actions[0], + ); + } + } + process.stdout.write(JSON.stringify(converted) + "\n"); +} +const init = fixture("text-turn")[0]; +if (args.includes("--model")) init.init.model = args[args.indexOf("--model") + 1]; +emit(init); +let interrupted = null; +const turns = new Map(); +const lines = createInterface({ input: process.stdin }); +lines.on("line", (line) => { + const input = JSON.parse(line); + record({ input }); + const text = input.message.content; + let name = "text-turn"; + if (text.includes("Attached image: ")) name = "image-file-external"; + if (text.includes("TOOLS")) name = "tools-default"; + if (text.includes("DENIAL_REPLAY")) name = "denial-replay"; + if (text.includes("SUBAGENT")) name = "subagent"; + if (text.includes("MULTI")) name = "multi-turn-reasoning"; + if (text.includes("RESUME")) name = "resume"; + if (text.includes("HANG")) name = "sigint-mid-text"; + if (text.includes("TOOL_HANG")) name = "sigint-mid-tool"; + const frames = fixture(name).slice(1); + if (name.startsWith("sigint")) { + interrupted = frames[frames.length - 1]; + for (const frame of frames.slice(0, -1)) emit(frame); + return; + } + const count = frames.filter((frame) => frame.event === "result").length; + const ordinal = (turns.get(name) || 0) % count; + turns.set(name, ordinal + 1); + let resultCount = 0; + for (const frame of frames) { + if (resultCount === ordinal) emit(frame); + if (frame.event === "result") resultCount += 1; + } +}); +process.on("SIGINT", () => { + record({ signal: "SIGINT" }); + if (process.env.AGY_TEST_IGNORE_SIGNALS === "yes") return; + if (interrupted) emit(interrupted); + process.exit(1); +}); +process.on("SIGTERM", () => { + record({ signal: "SIGTERM" }); + if (process.env.AGY_TEST_IGNORE_SIGNALS !== "yes") process.exit(1); +}); +lines.on("close", () => process.exit(0)); diff --git a/plugins/antigravity-provider/test/fixtures/README.md b/plugins/antigravity-provider/test/fixtures/README.md new file mode 100644 index 0000000000..b4b1ef264f --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/README.md @@ -0,0 +1,100 @@ +# agy 1.2.13 stream-json captures + +`denial-replay.stdout.ndjson` replays two turns from one real 1.2.13 process: a denied shell +command, then a text-only answer whose result retains the first denial. The replay clock is +synthetic; frames retain the native cumulative denial payload. + +Captured on Linux x86_64 on 2026-09-29. These are observations from the supplied binary, not guarantees for the Google sign-in route. No product code or capture drivers are included. + +Each row names a capture prefix. Its `.stdin.ndjson`, `.stdout.ndjson`, `.stderr.ndjson`, and `.capture.ndjson` companions keep the streams separate; empty streams are empty files. Every line wraps the exact decoded JSON frame or text line in `{t_ms, stream, data}`. `t_ms` is milliseconds since construction immediately before spawn. Capture annotations record launch arguments, configuration, signals, EOF, timeouts, and exit code; they are not CLI output. Line references refer to the indicated file, starting at 1. + +Commands below run from ``. `` expands to `/.local/opt/agy/1.2.13/agy` in the real installation home; the child uses a separate isolated `` for state. All captures remove inherited credential/provider environment variables, set `HOME`, `XDG_CONFIG_HOME=/.config`, `XDG_CACHE_HOME=/.cache`, `XDG_DATA_HOME=/.local/share`, `BROWSER=/bin/true`, and loopback-only `NO_PROXY`/`no_proxy`. Neither PATH nor shell rc files changed. Resume rows reuse the original isolated HOME. + +For rows marked **API**, the environment also contains `GEMINI_API_KEY=` and `GOOGLE_GEMINI_BASE_URL=http://127.0.0.1:`, and `/.gemini/antigravity-cli/settings.json` contains `{"modelProvider":"gemini"}`. **Env only** sets those two variables without the setting; **None** has neither. The loopback translator reused the prior QA route and forwarded real requests to a paid upstream model. Its credential was read programmatically inside the temporary proxy, with later captures passing it through the proxy environment. It was never passed to agy or recorded. The translator maps all requested Gemini models to one upstream Gemini model: request routing and CLI selection are verified, native model identity/quality is not. + +The translator emits text, function calls, thought parts and usage. Its usage mapping does not populate Gemini thinking/cache counts, so observed zero `thinking_tokens`/`cache_read_tokens` does not prove native zeros. No dedicated reasoning frames appeared, including the matched high-effort probe. + +``, ``, ``, ``, ``, and `` are redactions. Unrelated project/model names and help examples are redacted without removing frames. Delta chunk boundaries remain; a path split across deltas can have a different placeholder spelling than the complete result. UUIDs are retained for conversation correlation. + +| Fixture prefix | Exact argv (redacted) | Auth | Demonstrates | agy | +| --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | +| `agents` | ` agents` | API | Available agents listing is plain text. | 1.2.13 | +| `api-key-env-only` | ` --input-format stream-json --output-format stream-json` | Env only | Dummy key and custom endpoint alone do not select API-key auth. | 1.2.13 | +| `api-key-settings` | ` --input-format stream-json --output-format stream-json` | API | modelProvider=gemini yields init; missing event input is rejected. | 1.2.13 | +| `cwd-without-add-dir` | ` --input-format stream-json --output-format stream-json --dangerously-skip-permissions` | API | Without add-dir, model-issued pwd returned launch cwd; required tool Cwd is not exposed by this wire frame. | 1.2.13 | +| `effort-unsupported-family` | ` --input-format stream-json --output-format stream-json --model '' --effort high` | API | Unsupported non-Gemini family rejects effort; model reference redacted. | 1.2.13 | +| `effort-encoded-match` | ` --input-format stream-json --output-format stream-json --model gemini-3.8-flash-high --effort high` | API | Matching high effort accepted; arithmetic turn; no thought delta observed. | 1.2.13 | +| `effort-encoded-mismatch` | ` --input-format stream-json --output-format stream-json --model gemini-3.8-flash-high --effort low` | API | High-suffixed model conflicts with effort=low; no init. | 1.2.13 | +| `effort-mismatch` | ` --input-format stream-json --output-format stream-json --model gemini-3.1-pro-preview --effort low` | API | Unlisted preview ID does not support effort; startup ERROR. | 1.2.13 | +| `help` | ` --help` | None | Launch flags and subcommands; help is on stderr. | 1.2.13 | +| `image-content` | ` --input-format stream-json --output-format stream-json --disable-slash-commands` | API | Image source block rejected: only text blocks supported. | 1.2.13 | +| `mcp-add-http` | ` mcp add phase0 'http://127.0.0.1:/mcp'` | None | Writes .gemini/config/mcp_config.json; file content recorded in capture annotations. | 1.2.13 | +| `mcp-add` | ` mcp add phase0 --url 'http://127.0.0.1:/mcp'` | None | Invalid --url syntax rejected; no configuration written. | 1.2.13 | +| `mcp-global` | ` --input-format stream-json --output-format stream-json --add-dir '' --disable-slash-commands --dangerously-skip-permissions` | API | Global HTTP server discovered; strict tool call fails because arguments is a string. | 1.2.13 | +| `mcp-help` | ` mcp add --help` | None | MCP add syntax, HTTP transport, headers/env; flags precede name. | 1.2.13 | +| `mcp-list` | ` mcp list` | None | Model-free empty server list, exit 0. | 1.2.13 | +| `mcp-project-config` | ` --input-format stream-json --output-format stream-json --add-dir '' --disable-slash-commands --dangerously-skip-permissions` | API | Workspace .gemini/config.json mcpServers not discovered in this probe. | 1.2.13 | +| `mcp-project-isolated` | ` --input-format stream-json --output-format stream-json --add-dir '' --dangerously-skip-permissions` | API | Project MCP discovery; no global MCP config before/after; strict arguments error. | 1.2.13 | +| `mcp-project` | ` --input-format stream-json --output-format stream-json --add-dir '' --disable-slash-commands --dangerously-skip-permissions` | API | Project .agents/mcp_config.json discovers HTTP server; strict call rejects string arguments; later translator 502 retained. | 1.2.13 | +| `mcp-second-add-dir` | ` --input-format stream-json --output-format stream-json --disable-slash-commands --dangerously-skip-permissions --add-dir '' --add-dir '/owned'` | API | Discovers project MCP from the owned directory; strict arguments fail. Shell uses owned directory and writes occur in both directories. | 1.2.13 | +| `mcp-owned-first-add-dir` | ` --input-format stream-json --output-format stream-json --disable-slash-commands --dangerously-skip-permissions --add-dir '/owned' --add-dir ''` | API | Reversing argv does not restore command working directory; owned directory remains the shell default. | 1.2.13 | +| `mcp-owned-cwd` | ` --input-format stream-json --output-format stream-json --disable-slash-commands --dangerously-skip-permissions --add-dir ''`, launched in `/owned` through a symlink | API | Discovers owned project MCP, but shell and initial read resolve against owned cwd; writes occur in both directories. | 1.2.13 | +| `mcp-agent-file` | ` --input-format stream-json --output-format stream-json --disable-slash-commands --dangerously-skip-permissions --add-dir '' --agent '/owned/agent.md'` | API | Direct external agent filename falls back to default; no MCP discovery. Shell/read/write use workspace. | 1.2.13 | +| `mcp-agent-definition` | ` --input-format stream-json --output-format stream-json --add-dir '/owned' --add-dir '' --agent phase2 --dangerously-skip-permissions` | API | Nested Markdown definition with inline mcpServers is not found; default agent, no MCP server. | 1.2.13 | +| `mcp-agent-discovered` | ` --input-format stream-json --output-format stream-json --add-dir '/owned' --add-dir '' --agent phase2 --dangerously-skip-permissions` | API | Flat Markdown definition plus agents.json manifest is also not found; default agent, no MCP server. | 1.2.13 | +| `image-file-external` | ` --input-format stream-json --output-format stream-json --add-dir '' --disable-slash-commands` | API | Default mode reads an external image path with view_file and correctly describes its pixels, without an extra add-dir. | 1.2.13 | +| `model-switch-invalid` | ` --input-format stream-json --output-format stream-json --add-dir '' --conversation b966b16a-caf4-448a-bb24-0e34956022b0 --model gemini-3.1-flash-lite-preview` | API | Unlisted model ID rejected before init. | 1.2.13 | +| `model-switch-network` | ` --input-format stream-json --output-format stream-json --conversation b966b16a-caf4-448a-bb24-0e34956022b0 --model gemini-3.8-flash-low` | API | Listed model respawn plus actual Gemini request path in capture annotations; COBALT preserved. | 1.2.13 | +| `model-switch-resume` | ` --input-format stream-json --output-format stream-json --add-dir '' --conversation b966b16a-caf4-448a-bb24-0e34956022b0 --model gemini-3.1-flash-high` | API | Another unlisted model ID rejected before init. | 1.2.13 | +| `model-switch-valid` | ` --input-format stream-json --output-format stream-json --conversation b966b16a-caf4-448a-bb24-0e34956022b0 --model gemini-3.8-flash-low` | API | Listed low-effort model accepted on same conversation; COBALT preserved. | 1.2.13 | +| `models-api-key` | ` models` | API | Authenticated catalog is tab-separated model ID and display label. | 1.2.13 | +| `models-unauthenticated` | ` models` | None | Models requires auth; no catalog on stdout. | 1.2.13 | +| `multi-turn-reasoning` | ` --input-format stream-json --output-format stream-json --add-dir '' --disable-slash-commands` | API | Two text turns with arithmetic; per-generation and cumulative usage. No thought delta observed. | 1.2.13 | +| `not-signed-in` | ` --input-format stream-json --output-format stream-json` | None | No credentials: result ERROR, no init, exit 1. | 1.2.13 | +| `project-skill` | ` --input-format stream-json --output-format stream-json --add-dir '' --disable-slash-commands` | API | Project skill read via view_file; marker reply. No distinct skill event. | 1.2.13 | +| `question-auto-denial` | ` --input-format stream-json --output-format stream-json --add-dir ''` | API | Requested ask_question becomes unknown DONE step, auto-skipped; no question payload on wire. | 1.2.13 | +| `resume-after-interrupt` | ` --input-format stream-json --output-format stream-json --conversation a2c2132f-0f10-400b-a142-74f82230f449` | API | Same interrupted conversation resumes and accepts another turn. | 1.2.13 | +| `resume` | ` --input-format stream-json --output-format stream-json --add-dir '' --disable-slash-commands --conversation b966b16a-caf4-448a-bb24-0e34956022b0` | API | Remembered COBALT after respawn; no old steps replayed. | 1.2.13 | +| `schema-prompt` | ` --input-format stream-json --output-format stream-json` | API | Unknown prompt event ignored with stderr warning; no turn. | 1.2.13 | +| `schema-user-message` | ` --input-format stream-json --output-format stream-json` | API | Decoding a string message fails before event dispatch. | 1.2.13 | +| `schema-user` | ` --input-format stream-json --output-format stream-json` | API | String message rejected; message must be an object. | 1.2.13 | +| `sigint-mid-text` | ` --input-format stream-json --output-format stream-json --add-dir '' --disable-slash-commands --dangerously-skip-permissions` | API | SIGINT after first text delta: interrupted result and exit 1. | 1.2.13 | +| `sigint-mid-tool` | ` --input-format stream-json --output-format stream-json --add-dir '' --dangerously-skip-permissions` | API | SIGINT while run_command ACTIVE: interrupted result and exit 1. | 1.2.13 | +| `sigterm` | ` --input-format stream-json --output-format stream-json --add-dir '' --disable-slash-commands --dangerously-skip-permissions` | API | SIGTERM after user_input: interrupted result and exit 1. | 1.2.13 | +| `slash-disabled` | ` --input-format stream-json --output-format stream-json --disable-slash-commands` | API | /help sent literally to model; normal turn with built-in skill reads. | 1.2.13 | +| `slash-enabled` | ` --input-format stream-json --output-format stream-json` | API | /help rejected in driver mode; result ERROR and exit 2. | 1.2.13 | +| `stdin-close-mid-turn` | ` --input-format stream-json --output-format stream-json --add-dir '' --disable-slash-commands --dangerously-skip-permissions` | API | EOF after user_input: active turn finishes successfully, exit 0. | 1.2.13 | +| `stdin-mid-turn` | ` --input-format stream-json --output-format stream-json --add-dir '' --disable-slash-commands --dangerously-skip-permissions` | API | Second prompt after first user_input queues a separate turn; two results. | 1.2.13 | +| `subagent` | ` --input-format stream-json --output-format stream-json --add-dir '' --dangerously-skip-permissions` | API | Define/invoke child; subagent_info contains child ID/log URI/workspaces; child reply reaches parent. | 1.2.13 | +| `text-block` | ` --input-format stream-json --output-format stream-json --add-dir ''` | API | Array of text content blocks accepted. | 1.2.13 | +| `text-turn` | ` --input-format stream-json --output-format stream-json --add-dir ''` | API | event=user, message.content string; streaming text and final usage. | 1.2.13 | +| `tools-accept-edits` | ` --input-format stream-json --output-format stream-json --add-dir '' --disable-slash-commands --mode accept-edits` | API | Read, create, edit allowed; shell auto-denied. Result SUCCESS includes denied_actions. No permission request. | 1.2.13 | +| `tools-default` | ` --input-format stream-json --output-format stream-json --add-dir '' --disable-slash-commands` | API | Read, create, edit allowed; shell auto-denied. Result SUCCESS includes denied_actions. No permission request. | 1.2.13 | +| `tools-plan` | ` --input-format stream-json --output-format stream-json --add-dir '' --disable-slash-commands --mode plan` | API | Read, create, edit allowed; shell auto-denied. Result SUCCESS includes denied_actions. No permission request. | 1.2.13 | +| `tools-sandbox` | ` --input-format stream-json --output-format stream-json --add-dir '' --disable-slash-commands --sandbox` | API | Read, create, edit allowed; shell auto-denied. Result SUCCESS includes denied_actions. No permission request. | 1.2.13 | +| `tools-skip-permissions` | ` --input-format stream-json --output-format stream-json --add-dir '' --disable-slash-commands --dangerously-skip-permissions` | API | Read, create, edit and shell succeed; permission_mode=always-proceed. | 1.2.13 | +| `version` | ` --version` | None | Installed binary reports 1.2.13. | 1.2.13 | +| `mcp-http.requests.ndjson` | `node /mcp-server.mjs` alongside the MCP argv above | None | Actual HTTP JSON-RPC requests to a strict SDK server; `params.arguments` is a string. Server implements initialize, tools/list, and echo. | 1.2.13 client | +| `state-inspection.ndjson` | `HOME= strace -f -e trace=file -o /files.trace --input-format stream-json --output-format stream-json`; `python3 /finalize.py`; `firecrawl scrape --format markdown` | None | Binary checksum, isolated state accesses, read-only SQLite listings, written file contents, and manifest retrieval caveat. Inspection data, not agy stdout. | 1.2.13 | + +`project-skill` used `/.agents/skills/phase0-marker/SKILL.md` with name `phase0-marker`, description “Return the Phase 0 marker when requested,” and instructions “Respond with PHASE0_SKILL_MARKER. No tools are needed after reading this skill.” + +Each tools capture begins with `input.txt` containing `PHASE0_READ_MARKER`, then requests create→replace from `PHASE0_WRITE` to `PHASE0_EDIT`, then `printf PHASE0_SHELL`. `state-inspection.ndjson` records the resulting files. These successful writes disprove an enforced read-only boundary for default/plan on this credential route. `--sandbox` alone still denies the shell at permission policy, so OS sandbox enforcement is unverified. + +## MCP discovery + +Phase 0 project setup (`mcp-project` and `mcp-project-isolated`) places `{"mcpServers":{"phase0":{"serverUrl":"http://127.0.0.1:/mcp"}}}` in `/.agents/mcp_config.json`. `mcp-project-config` instead uses `/.gemini/config.json`; that file did not expose the server. HTTP calls send string arguments and fail strict validation. + +Phase 2 places the same HTTP configuration, named `phase2`, in a plugin-owned `/owned/.agents/mcp_config.json`. Both add-dir orders discover it, as does launching in the owned directory and adding the real workspace. All three turns execute shell `pwd` in the owned directory. The file turns read the workspace marker but also write `output.txt` into the owned directory. These routes fail the requirement that tools resolve against the user's workspace. Reordering arguments did not change the observed shell cwd; the precise directory selection rule is unverified. + +The external `--agent` file, nested agent definition, and flat definition with an `agents.json` manifest contain inline `mcpServers`. Native logs record “not found, falling back to default” for each. These experiments do not establish that every agent definition format is unsupported. `parity-inspection.ndjson` records their definitions and log evidence. Searching the tested binary's strings found no `AGY_MCP`, `MCP_CONFIG`, `AGY_CONFIG`, or `ANTIGRAVITY_CONFIG` override; no environment discovery route was verified. + +No tested MCP route satisfies both discovery and workspace ownership. MCP implementation stopped at this gate: no relay or MCP capability change was added. Images and the daemon e2e continued independently. No global MCP configuration exists in the isolated homes, and no configuration was written into the user repository. + +## Image file fallback + +`image-file-external.png` contains a red triangle on the left and a blue circle on the lower right, against white. The prompt supplies only its path and asks for visible shapes and colors. `image-file-external.stdout.ndjson:4–5` records a successful `view_file`; the result describes the pixels correctly. The native function response supplies bytes at `functionResponse.parts[].inlineData`. The temporary translator was corrected to forward these nested parts to the model; forwarding top-level parts alone dropped the image. `parity-inspection.ndjson` records the image hash and response-part location. + +This verifies one default-mode CLI turn reading a file outside the workspace without an extra add-dir. A later isolated-daemon run attached the same image through the provider and returned the same shapes and colors after a successful `view_file`. The provider advertises `prompt.image`; MCP remains outside the parity implementation. + +Unverified: Google login behaviour; native reasoning/cached-token accounting; filesystem/network isolation under sandbox plus approved commands; exhaustive tool policy; other agent definition formats or environment MCP overrides; child transcript streaming beyond parent metadata; a stable public conversation-list command. No fresh login was created. The fetched official manifest returned 1.2.12 while this binary reports 1.2.13; the checksum identifies the tested binary, but that response does not establish its release provenance. diff --git a/plugins/antigravity-provider/test/fixtures/agents.capture.ndjson b/plugins/antigravity-provider/test/fixtures/agents.capture.ndjson new file mode 100644 index 0000000000..cc01e91070 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/agents.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "agents"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 309, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/agents.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/agents.stderr.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/agents.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/agents.stdin.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/agents.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/agents.stdout.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/api-key-env-only.capture.ndjson b/plugins/antigravity-provider/test/fixtures/api-key-env-only.capture.ndjson new file mode 100644 index 0000000000..9c7a770416 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/api-key-env-only.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json"], "cwd": "", "home": "", "settings": null, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 452, "stream": "capture", "data": {"exit_code": 1}} diff --git a/plugins/antigravity-provider/test/fixtures/api-key-env-only.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/api-key-env-only.stderr.ndjson new file mode 100644 index 0000000000..d315088707 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/api-key-env-only.stderr.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 288, "stream": "stderr", "data": "Error: authentication required. Run 'agy' to log in, then retry."} +{"t_ms": 288, "stream": "stderr", "data": "error: authentication failed or timed out"} diff --git a/plugins/antigravity-provider/test/fixtures/api-key-env-only.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/api-key-env-only.stdin.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/api-key-env-only.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/api-key-env-only.stdout.ndjson new file mode 100644 index 0000000000..60cc38245f --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/api-key-env-only.stdout.ndjson @@ -0,0 +1 @@ +{"t_ms": 288, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "", "status": "ERROR", "response": "", "error": "authentication failed or timed out", "duration_seconds": 0, "num_turns": 0, "usage": {"input_tokens": 0, "output_tokens": 0, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 0}}}} diff --git a/plugins/antigravity-provider/test/fixtures/api-key-settings.capture.ndjson b/plugins/antigravity-provider/test/fixtures/api-key-settings.capture.ndjson new file mode 100644 index 0000000000..1c03483e72 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/api-key-settings.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 525, "stream": "capture", "data": {"exit_code": 1}} diff --git a/plugins/antigravity-provider/test/fixtures/api-key-settings.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/api-key-settings.stderr.ndjson new file mode 100644 index 0000000000..1c47437a40 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/api-key-settings.stderr.ndjson @@ -0,0 +1 @@ +{"t_ms": 374, "stream": "stderr", "data": "error: stream input message is missing the \"event\" field"} diff --git a/plugins/antigravity-provider/test/fixtures/api-key-settings.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/api-key-settings.stdin.ndjson new file mode 100644 index 0000000000..97362f53ed --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/api-key-settings.stdin.ndjson @@ -0,0 +1 @@ +{"t_ms": 373, "stream": "stdin", "data": {"type": "user", "message": {"role": "user", "content": "Reply with PHASE0_HELLO. No tools."}}} diff --git a/plugins/antigravity-provider/test/fixtures/api-key-settings.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/api-key-settings.stdout.ndjson new file mode 100644 index 0000000000..567b1fc1f1 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/api-key-settings.stdout.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 373, "stream": "stdout", "data": {"event": "init", "conversation_id": "e629b41a-decf-4212-a9fb-3809e8f21122", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "request-review"}}} +{"t_ms": 374, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "e629b41a-decf-4212-a9fb-3809e8f21122", "status": "ERROR", "response": "", "error": "stream input message is missing the \"event\" field", "duration_seconds": 0, "num_turns": 0, "usage": {"input_tokens": 0, "output_tokens": 0, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 0}}}} diff --git a/plugins/antigravity-provider/test/fixtures/cwd-without-add-dir.capture.ndjson b/plugins/antigravity-provider/test/fixtures/cwd-without-add-dir.capture.ndjson new file mode 100644 index 0000000000..78eedeec6f --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/cwd-without-add-dir.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--dangerously-skip-permissions"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 5003, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/cwd-without-add-dir.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/cwd-without-add-dir.stderr.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/cwd-without-add-dir.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/cwd-without-add-dir.stdin.ndjson new file mode 100644 index 0000000000..52514739aa --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/cwd-without-add-dir.stdin.ndjson @@ -0,0 +1 @@ +{"t_ms": 318, "stream": "stdin", "data": {"event": "user", "message": {"content": "Use run_command to run pwd, WaitMsBeforeAsync 1000. Report the output."}}} diff --git a/plugins/antigravity-provider/test/fixtures/cwd-without-add-dir.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/cwd-without-add-dir.stdout.ndjson new file mode 100644 index 0000000000..469cdadcca --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/cwd-without-add-dir.stdout.ndjson @@ -0,0 +1,11 @@ +{"t_ms": 318, "stream": "stdout", "data": {"event": "init", "conversation_id": "3cea6151-badf-4b4a-ad58-bf3c0b02db25", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "always-proceed"}}} +{"t_ms": 347, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "3cea6151-badf-4b4a-ad58-bf3c0b02db25", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 3066, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "3cea6151-badf-4b4a-ad58-bf3c0b02db25", "step_index": 1, "state": "DONE", "step_type": "agent_response", "duration_seconds": 2.698205516, "usage": {"input_tokens": 11255, "output_tokens": 138, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11393}}}} +{"t_ms": 3066, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "3cea6151-badf-4b4a-ad58-bf3c0b02db25", "step_index": 2, "state": "ACTIVE", "step_type": "tool", "tool_name": "run_command", "tool_info": {"name": "run_command", "parameters": {"CommandLine": "pwd"}}}}} +{"t_ms": 3066, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "3cea6151-badf-4b4a-ad58-bf3c0b02db25", "step_index": 2, "state": "DONE", "step_type": "tool", "tool_name": "run_command", "duration_seconds": 0.006078385, "tool_info": {"name": "run_command", "parameters": {"CommandLine": "pwd"}, "output": "\r\n"}}}} +{"t_ms": 4626, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "3cea6151-badf-4b4a-ad58-bf3c0b02db25", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "The output"}}} +{"t_ms": 4678, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "3cea6151-badf-4b4a-ad58-bf3c0b02db25", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " of running "}}} +{"t_ms": 4704, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "3cea6151-badf-4b4a-ad58-bf3c0b02db25", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "`pwd` is:\n\n```text\n/works"}}} +{"t_ms": 4729, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "3cea6151-badf-4b4a-ad58-bf3c0b02db25", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "pace\n```"}}} +{"t_ms": 4881, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "3cea6151-badf-4b4a-ad58-bf3c0b02db25", "step_index": 3, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 1.824928838, "usage": {"input_tokens": 11382, "output_tokens": 24, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11406}}}} +{"t_ms": 4881, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "3cea6151-badf-4b4a-ad58-bf3c0b02db25", "status": "SUCCESS", "response": "The output of running `pwd` is:\n\n```text\n\n```\n", "duration_seconds": 4.555206291, "num_turns": 1, "usage": {"input_tokens": 22637, "output_tokens": 162, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 22799}}}} diff --git a/plugins/antigravity-provider/test/fixtures/denial-replay.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/denial-replay.stdout.ndjson new file mode 100644 index 0000000000..b667b505cc --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/denial-replay.stdout.ndjson @@ -0,0 +1,11 @@ +{"t_ms":0,"stream":"stdout","data":{"event":"init","conversation_id":"e8243164-64d5-4419-9dbb-c0733300b557","init":{"cwd":"/tmp/agy-p3/workspace","tools":["ask_custom_permission","ask_permission","ask_question","browser_click_element","browser_drag_pixel_to_pixel","browser_get_dom","browser_get_network_request","browser_input","browser_list_network_requests","browser_mouse_down","browser_mouse_up","browser_move_mouse","browser_press_key","browser_refresh_page","browser_resize_window","browser_scroll","browser_scroll_dom","browser_select_option","browser_subagent","call_mcp_tool","capture_browser_console_logs","capture_browser_screenshot","click_browser_pixel","command_status","define_subagent","delete_knowledge","execute_browser_javascript","find_by_name","finish","generate_image","grep_search","invoke_subagent","list_browser_pages","list_dir","list_permissions","list_resources","manage_inbox","manage_subagents","manage_task","multi_replace_file_content","notebook_edit","notebook_execution","open_browser_url","read_browser_page","read_resource","read_url_content","replace_file_content","run_command","schedule","search_web","sed_file","send_command_input","send_message","view_file","wait","wait_5_seconds","write_to_file"],"permission_mode":"request-review"}}} +{"t_ms":1,"stream":"stdout","data":{"event":"step_update","step_update":{"conversation_id":"e8243164-64d5-4419-9dbb-c0733300b557","step_index":0,"state":"DONE","step_type":"user_input"}}} +{"t_ms":2,"stream":"stdout","data":{"event":"step_update","step_update":{"conversation_id":"e8243164-64d5-4419-9dbb-c0733300b557","step_index":1,"state":"DONE","step_type":"agent_response","duration_seconds":2.86246236,"usage":{"input_tokens":11252,"output_tokens":163,"thinking_tokens":0,"cache_read_tokens":0,"total_tokens":11415}}}} +{"t_ms":3,"stream":"stdout","data":{"event":"step_update","step_update":{"conversation_id":"e8243164-64d5-4419-9dbb-c0733300b557","step_index":2,"state":"ACTIVE","step_type":"tool","tool_name":"run_command","tool_info":{"name":"run_command","parameters":{"CommandLine":"printf AGY_DENIAL_PROBE"}}}}} +{"t_ms":4,"stream":"stdout","data":{"event":"step_update","step_update":{"conversation_id":"e8243164-64d5-4419-9dbb-c0733300b557","step_index":2,"state":"ERROR","step_type":"tool","tool_name":"run_command","duration_seconds":0.002264915,"tool_info":{"name":"run_command","parameters":{"CommandLine":"printf AGY_DENIAL_PROBE"},"error":{"type":"TOOL_ERROR","message":"permission check failed for command \"printf AGY_DENIAL_PROBE\": user denied permission to run command:\nprintf AGY_DENIAL_PROBE\nDo not attempt to circumvent this denial by rephrasing the command, using alternative tools/scripts (e.g. python, sh, curl), or accessing the same target resource. Proceed without performing this action."}}}}} +{"t_ms":5,"stream":"stdout","data":{"event":"result","result":{"conversation_id":"e8243164-64d5-4419-9dbb-c0733300b557","status":"SUCCESS","response":"","duration_seconds":2.890969963,"num_turns":1,"usage":{"input_tokens":11252,"output_tokens":163,"thinking_tokens":0,"cache_read_tokens":0,"total_tokens":11415},"denied_actions":[{"action":"command","display_name":"RunCommand"}]}}} +{"t_ms":6,"stream":"stdout","data":{"event":"step_update","step_update":{"conversation_id":"e8243164-64d5-4419-9dbb-c0733300b557","step_index":3,"state":"DONE","step_type":"user_input"}}} +{"t_ms":7,"stream":"stdout","data":{"event":"step_update","step_update":{"conversation_id":"e8243164-64d5-4419-9dbb-c0733300b557","step_index":4,"state":"ACTIVE","step_type":"agent_response","text_delta":"FOLLOWUP"}}} +{"t_ms":8,"stream":"stdout","data":{"event":"step_update","step_update":{"conversation_id":"e8243164-64d5-4419-9dbb-c0733300b557","step_index":4,"state":"ACTIVE","step_type":"agent_response","text_delta":"_OK"}}} +{"t_ms":9,"stream":"stdout","data":{"event":"step_update","step_update":{"conversation_id":"e8243164-64d5-4419-9dbb-c0733300b557","step_index":4,"state":"DONE","step_type":"agent_response","text_delta":"\n","duration_seconds":2.11565723,"usage":{"input_tokens":11533,"output_tokens":4,"thinking_tokens":0,"cache_read_tokens":0,"total_tokens":11537}}}} +{"t_ms":10,"stream":"stdout","data":{"event":"result","result":{"conversation_id":"e8243164-64d5-4419-9dbb-c0733300b557","status":"SUCCESS","response":"FOLLOWUP_OK\n","duration_seconds":5.028454184,"num_turns":2,"usage":{"input_tokens":22785,"output_tokens":167,"thinking_tokens":0,"cache_read_tokens":0,"total_tokens":22952},"denied_actions":[{"action":"command","display_name":"RunCommand"}]}}} diff --git a/plugins/antigravity-provider/test/fixtures/effort-encoded-match.capture.ndjson b/plugins/antigravity-provider/test/fixtures/effort-encoded-match.capture.ndjson new file mode 100644 index 0000000000..89227b65a3 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/effort-encoded-match.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--model", "gemini-3.8-flash-high", "--effort", "high"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 4787, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/effort-encoded-match.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/effort-encoded-match.stderr.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/effort-encoded-match.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/effort-encoded-match.stdin.ndjson new file mode 100644 index 0000000000..817c98ad5b --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/effort-encoded-match.stdin.ndjson @@ -0,0 +1 @@ +{"t_ms": 296, "stream": "stdin", "data": {"event": "user", "message": {"content": "Solve 143*127. Think carefully before answering. No tools."}}} diff --git a/plugins/antigravity-provider/test/fixtures/effort-encoded-match.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/effort-encoded-match.stdout.ndjson new file mode 100644 index 0000000000..e56f554df9 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/effort-encoded-match.stdout.ndjson @@ -0,0 +1,19 @@ +{"t_ms": 295, "stream": "stdout", "data": {"event": "init", "conversation_id": "a090b266-2ea9-418c-bb02-c3267268b67b", "init": {"model": "gemini-3.8-flash-high", "cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "request-review"}}} +{"t_ms": 323, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a090b266-2ea9-418c-bb02-c3267268b67b", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 3877, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a090b266-2ea9-418c-bb02-c3267268b67b", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "**18,161**\n\n### Step-by-"}}} +{"t_ms": 3903, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a090b266-2ea9-418c-bb02-c3267268b67b", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "Step Breakdo"}}} +{"t_ms": 3928, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a090b266-2ea9-418c-bb02-c3267268b67b", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "wn:\nUsing the difference"}}} +{"t_ms": 3953, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a090b266-2ea9-418c-bb02-c3267268b67b", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " of squares "}}} +{"t_ms": 3979, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a090b266-2ea9-418c-bb02-c3267268b67b", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "formula $("}}} +{"t_ms": 4055, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a090b266-2ea9-418c-bb02-c3267268b67b", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "a + b)(a - b) = a^2 - b^2$:\n- $143 ="}}} +{"t_ms": 4081, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a090b266-2ea9-418c-bb02-c3267268b67b", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " 135"}}} +{"t_ms": 4132, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a090b266-2ea9-418c-bb02-c3267268b67b", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " + 8$\n- $127"}}} +{"t_ms": 4157, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a090b266-2ea9-418c-bb02-c3267268b67b", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " = 135 - 8$\n\n$$143"}}} +{"t_ms": 4259, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a090b266-2ea9-418c-bb02-c3267268b67b", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " \\times 127 = 135^2 - 8^"}}} +{"t_ms": 4284, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a090b266-2ea9-418c-bb02-c3267268b67b", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "2$$\n$$135"}}} +{"t_ms": 4385, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a090b266-2ea9-418c-bb02-c3267268b67b", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "^2 = 18,225$$\n$$8^2 = 64"}}} +{"t_ms": 4411, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a090b266-2ea9-418c-bb02-c3267268b67b", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "$$\n$$18,22"}}} +{"t_ms": 4462, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a090b266-2ea9-418c-bb02-c3267268b67b", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "5 - 64 = \\ma"}}} +{"t_ms": 4487, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a090b266-2ea9-418c-bb02-c3267268b67b", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "thbf{18,161}$$"}}} +{"t_ms": 4640, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a090b266-2ea9-418c-bb02-c3267268b67b", "step_index": 1, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 4.300859666, "usage": {"input_tokens": 10982, "output_tokens": 553, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11535}}}} +{"t_ms": 4640, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "a090b266-2ea9-418c-bb02-c3267268b67b", "status": "SUCCESS", "response": "**18,161**\n\n### Step-by-Step Breakdown:\nUsing the difference of squares formula $(a + b)(a - b) = a^2 - b^2$:\n- $143 = 135 + 8$\n- $127 = 135 - 8$\n\n$$143 \\times 127 = 135^2 - 8^2$$\n$$135^2 = 18,225$$\n$$8^2 = 64$$\n$$18,225 - 64 = \\mathbf{18,161}$$\n", "duration_seconds": 4.326461265, "num_turns": 1, "usage": {"input_tokens": 10982, "output_tokens": 553, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11535}}}} diff --git a/plugins/antigravity-provider/test/fixtures/effort-encoded-mismatch.capture.ndjson b/plugins/antigravity-provider/test/fixtures/effort-encoded-mismatch.capture.ndjson new file mode 100644 index 0000000000..22f8817b70 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/effort-encoded-mismatch.capture.ndjson @@ -0,0 +1,3 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--model", "gemini-3.8-flash-high", "--effort", "low"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 5006, "stream": "capture", "data": {"timeout_waiting_for": "init", "seconds": 5}} +{"t_ms": 5006, "stream": "capture", "data": {"exit_code": 1}} diff --git a/plugins/antigravity-provider/test/fixtures/effort-encoded-mismatch.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/effort-encoded-mismatch.stderr.ndjson new file mode 100644 index 0000000000..1807a73321 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/effort-encoded-mismatch.stderr.ndjson @@ -0,0 +1 @@ +{"t_ms": 296, "stream": "stderr", "data": "error: invalid model selection (--model \"gemini-3.8-flash-high\" --effort \"low\"): --model gemini-3.8-flash-high conflicts with --effort=low"} diff --git a/plugins/antigravity-provider/test/fixtures/effort-encoded-mismatch.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/effort-encoded-mismatch.stdin.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/effort-encoded-mismatch.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/effort-encoded-mismatch.stdout.ndjson new file mode 100644 index 0000000000..b9669aaaf4 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/effort-encoded-mismatch.stdout.ndjson @@ -0,0 +1 @@ +{"t_ms": 296, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "", "status": "ERROR", "response": "", "error": "invalid model selection (--model \"gemini-3.8-flash-high\" --effort \"low\"): --model gemini-3.8-flash-high conflicts with --effort=low", "duration_seconds": 0, "num_turns": 0, "usage": {"input_tokens": 0, "output_tokens": 0, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 0}}}} diff --git a/plugins/antigravity-provider/test/fixtures/effort-mismatch.capture.ndjson b/plugins/antigravity-provider/test/fixtures/effort-mismatch.capture.ndjson new file mode 100644 index 0000000000..fbee5c5bc5 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/effort-mismatch.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--model", "gemini-3.1-pro-preview", "--effort", "low"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 415, "stream": "capture", "data": {"exit_code": 1}} diff --git a/plugins/antigravity-provider/test/fixtures/effort-mismatch.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/effort-mismatch.stderr.ndjson new file mode 100644 index 0000000000..6072d28192 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/effort-mismatch.stderr.ndjson @@ -0,0 +1 @@ +{"t_ms": 298, "stream": "stderr", "data": "error: invalid model selection (--model \"gemini-3.1-pro-preview\" --effort \"low\"): --effort is not supported for model \"gemini-3.1-pro-preview\""} diff --git a/plugins/antigravity-provider/test/fixtures/effort-mismatch.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/effort-mismatch.stdin.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/effort-mismatch.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/effort-mismatch.stdout.ndjson new file mode 100644 index 0000000000..c0baa6d698 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/effort-mismatch.stdout.ndjson @@ -0,0 +1 @@ +{"t_ms": 298, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "", "status": "ERROR", "response": "", "error": "invalid model selection (--model \"gemini-3.1-pro-preview\" --effort \"low\"): --effort is not supported for model \"gemini-3.1-pro-preview\"", "duration_seconds": 0, "num_turns": 0, "usage": {"input_tokens": 0, "output_tokens": 0, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 0}}}} diff --git a/plugins/antigravity-provider/test/fixtures/effort-unsupported-family.capture.ndjson b/plugins/antigravity-provider/test/fixtures/effort-unsupported-family.capture.ndjson new file mode 100644 index 0000000000..47eee607d9 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/effort-unsupported-family.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--model", "", "--effort", "high"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 358, "stream": "capture", "data": {"exit_code": 1}} diff --git a/plugins/antigravity-provider/test/fixtures/effort-unsupported-family.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/effort-unsupported-family.stderr.ndjson new file mode 100644 index 0000000000..f19c83a09b --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/effort-unsupported-family.stderr.ndjson @@ -0,0 +1 @@ +{"t_ms": 287, "stream": "stderr", "data": "error: invalid model selection (--model \"\" --effort \"high\"): --effort is not supported for model \"\""} diff --git a/plugins/antigravity-provider/test/fixtures/effort-unsupported-family.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/effort-unsupported-family.stdin.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/effort-unsupported-family.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/effort-unsupported-family.stdout.ndjson new file mode 100644 index 0000000000..b85109a9a9 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/effort-unsupported-family.stdout.ndjson @@ -0,0 +1 @@ +{"t_ms": 287, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "", "status": "ERROR", "response": "", "error": "invalid model selection (--model \"\" --effort \"high\"): --effort is not supported for model \"\"", "duration_seconds": 0, "num_turns": 0, "usage": {"input_tokens": 0, "output_tokens": 0, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 0}}}} diff --git a/plugins/antigravity-provider/test/fixtures/help.capture.ndjson b/plugins/antigravity-provider/test/fixtures/help.capture.ndjson new file mode 100644 index 0000000000..6e88cd1f4f --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/help.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--help"], "cwd": "", "home": "", "settings": null, "env": {}}} +{"t_ms": 209, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/help.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/help.stderr.ndjson new file mode 100644 index 0000000000..3a8e68bf4b --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/help.stderr.ndjson @@ -0,0 +1,39 @@ +{"t_ms": 195, "stream": "stderr", "data": "Usage of agy:"} +{"t_ms": 195, "stream": "stderr", "data": " --add-dir Add a directory to the workspace (repeatable) (default [])"} +{"t_ms": 195, "stream": "stderr", "data": " --agent Agent for the current CLI session"} +{"t_ms": 195, "stream": "stderr", "data": " -c Short alias for --continue"} +{"t_ms": 195, "stream": "stderr", "data": " --continue Continue the most recent conversation"} +{"t_ms": 195, "stream": "stderr", "data": " --conversation Resume a previous conversation by ID"} +{"t_ms": 195, "stream": "stderr", "data": " --dangerously-skip-permissions Auto-approve all tool permission requests without prompting"} +{"t_ms": 195, "stream": "stderr", "data": " --disable-slash-commands Disable slash command and skill expansion in print mode"} +{"t_ms": 195, "stream": "stderr", "data": " --effort Reasoning effort for the current CLI session (low|medium|high|max)"} +{"t_ms": 195, "stream": "stderr", "data": " -i Short alias for --prompt-interactive"} +{"t_ms": 195, "stream": "stderr", "data": " --input-format Input format for print mode (text, stream-json). stream-json reads one NDJSON message per line from stdin and runs a turn for each; it requires --output-format stream-json (default text)"} +{"t_ms": 195, "stream": "stderr", "data": " --json-schema Optional JSON schema string or path to a schema file to enforce structured output (for stream-json, only applicable to the final result)"} +{"t_ms": 195, "stream": "stderr", "data": " --log-file Override CLI log file path"} +{"t_ms": 196, "stream": "stderr", "data": " --mode Set the agent execution mode for this session (accept-edits, plan)"} +{"t_ms": 196, "stream": "stderr", "data": " --model Model for the current CLI session"} +{"t_ms": 196, "stream": "stderr", "data": " --new-project Create a new project for this session"} +{"t_ms": 196, "stream": "stderr", "data": " --output-format Output format for print mode (text, json, stream-json) (default text)"} +{"t_ms": 196, "stream": "stderr", "data": " -p Short alias for --print"} +{"t_ms": 196, "stream": "stderr", "data": " --print Run a single prompt non-interactively and print the response"} +{"t_ms": 196, "stream": "stderr", "data": " --print-timeout Optional time limit for print mode; 0 waits until the turn completes (default 0s)"} +{"t_ms": 196, "stream": "stderr", "data": " --project Project ID or project name for the current CLI session"} +{"t_ms": 196, "stream": "stderr", "data": " --prompt Alias for --print"} +{"t_ms": 196, "stream": "stderr", "data": " --prompt-interactive Run an initial prompt interactively and continue the session"} +{"t_ms": 196, "stream": "stderr", "data": " --remote-control Create a remote connection for the CLI session on start up"} +{"t_ms": 196, "stream": "stderr", "data": " --sandbox Run in a sandbox with terminal restrictions enabled"} +{"t_ms": 196, "stream": "stderr", "data": ""} +{"t_ms": 196, "stream": "stderr", "data": "Available subcommands:"} +{"t_ms": 196, "stream": "stderr", "data": " agent List available agents"} +{"t_ms": 196, "stream": "stderr", "data": " agents List available agents"} +{"t_ms": 196, "stream": "stderr", "data": " changelog Show changelog and release notes"} +{"t_ms": 196, "stream": "stderr", "data": " help Show help for subcommands"} +{"t_ms": 196, "stream": "stderr", "data": " install Configure environment paths and shell settings"} +{"t_ms": 196, "stream": "stderr", "data": " mcp Manage MCP servers (add, remove, list, enable, disable)"} +{"t_ms": 196, "stream": "stderr", "data": " mic-serve Serve this machine's microphone to a CLI on another host"} +{"t_ms": 196, "stream": "stderr", "data": " models List available models"} +{"t_ms": 196, "stream": "stderr", "data": " plugin Manage plugins (install, uninstall, list, enable, disable)"} +{"t_ms": 196, "stream": "stderr", "data": " plugins Alias for plugin"} +{"t_ms": 196, "stream": "stderr", "data": " remote-control Manage the remote-control background daemon (start, status, stop)"} +{"t_ms": 196, "stream": "stderr", "data": " update Update CLI"} diff --git a/plugins/antigravity-provider/test/fixtures/help.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/help.stdin.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/help.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/help.stdout.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/image-content.capture.ndjson b/plugins/antigravity-provider/test/fixtures/image-content.capture.ndjson new file mode 100644 index 0000000000..a2351ec906 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/image-content.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--disable-slash-commands"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 383, "stream": "capture", "data": {"exit_code": 1}} diff --git a/plugins/antigravity-provider/test/fixtures/image-content.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/image-content.stderr.ndjson new file mode 100644 index 0000000000..3224b28fcc --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/image-content.stderr.ndjson @@ -0,0 +1 @@ +{"t_ms": 294, "stream": "stderr", "data": "error: stream input content block type \"image\" is not supported (only \"text\")"} diff --git a/plugins/antigravity-provider/test/fixtures/image-content.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/image-content.stdin.ndjson new file mode 100644 index 0000000000..fd1966ad02 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/image-content.stdin.ndjson @@ -0,0 +1 @@ +{"t_ms": 294, "stream": "stdin", "data": {"event": "user", "message": {"content": [{"type": "text", "text": "Describe this image."}, {"type": "image", "source": {"type": "base64", "media_type": "image/png", "data": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aN1sAAAAASUVORK5CYII="}}]}}} diff --git a/plugins/antigravity-provider/test/fixtures/image-content.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/image-content.stdout.ndjson new file mode 100644 index 0000000000..07cb4f0703 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/image-content.stdout.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 294, "stream": "stdout", "data": {"event": "init", "conversation_id": "71e19e79-63e3-4a7b-b628-822e69618edc", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "request-review"}}} +{"t_ms": 294, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "71e19e79-63e3-4a7b-b628-822e69618edc", "status": "ERROR", "response": "", "error": "stream input content block type \"image\" is not supported (only \"text\")", "duration_seconds": 0, "num_turns": 0, "usage": {"input_tokens": 0, "output_tokens": 0, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 0}}}} diff --git a/plugins/antigravity-provider/test/fixtures/image-file-external.capture.ndjson b/plugins/antigravity-provider/test/fixtures/image-file-external.capture.ndjson new file mode 100644 index 0000000000..90d75443bc --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/image-file-external.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", "", "--disable-slash-commands"], "cwd": "", "home": "/image-file-external-home", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 7877, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/image-file-external.png b/plugins/antigravity-provider/test/fixtures/image-file-external.png new file mode 100644 index 0000000000000000000000000000000000000000..de622fdd72dd12f017ac61ed74038f2e449c7422 GIT binary patch literal 910 zcmeAS@N?(olHy`uVBq!ia0vp^TR@nD2}o{QKQWbofjQLE#WAE}&fB{Ob3+_OTmso2 zDx9#_xb#CIGO1Bx!~c@t>{Uzns*Pi$^rMdqwSfOd*oe2kzpL`IortR@I9=kr) z_9R&u{!sRzmB1sZJaN$!O@(+?iM_8{PmOrNjOj~n7L zU9yKZhO)`e1}YS`;5+dk*&VF#cA?DVgU3Gsov7b;*yu-uv`km-k-&)uj~@aGiT5qy zu?qv*7F}>9^}|LL3z25$jbANZsr`r$mFdzw7I^&tSe|S`~`?KWu0*!A1eZMZ%-_uiWx@#_Uww$FLFSTA`pzr}-&ssA_CH@;H)_Os#f z!dFt;et{^F2NF?t59Y}i+*#kp(6{g^tIP++<7Z!eK5V%C*PjBOwfp``0_9fP*~k>H z=I1|t=hb15%!h(AYe^G&@#^;bX@v(1cLPlY>a~|yyqf!l*5jtf6JK#Wkcho|&`wfZ zhCggM!|@ldq|8q4cn}eE_XrW`a}6ZO@AWbS$uAX%xe3?$vxD+0-Q z|Fe&jEzG`MG19Z(`_iLh*Yr5x{86BgpZWbG>q$z&Br1NxF8WsJOxBLHOTc`_;OXk; Jvd$@?2>_QFt-JsL literal 0 HcmV?d00001 diff --git a/plugins/antigravity-provider/test/fixtures/image-file-external.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/image-file-external.stderr.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/image-file-external.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/image-file-external.stdin.ndjson new file mode 100644 index 0000000000..8dcd1a238a --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/image-file-external.stdin.ndjson @@ -0,0 +1 @@ +{"t_ms": 320, "stream": "stdin", "data": {"event": "user", "message": {"content": "Use view_file to open the attached image at /owned/attachment.png. Describe the shapes and colors visible. Do not use shell or code to infer the pixels."}}} diff --git a/plugins/antigravity-provider/test/fixtures/image-file-external.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/image-file-external.stdout.ndjson new file mode 100644 index 0000000000..77f1580b85 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/image-file-external.stdout.ndjson @@ -0,0 +1,14 @@ +{"t_ms": 320, "stream": "stdout", "data": {"event": "init", "conversation_id": "0c498203-4202-446f-8df1-dfa8f16cb19d", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "request-review"}}} +{"t_ms": 347, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0c498203-4202-446f-8df1-dfa8f16cb19d", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 4057, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0c498203-4202-446f-8df1-dfa8f16cb19d", "step_index": 1, "state": "DONE", "step_type": "agent_response", "duration_seconds": 3.704260379, "usage": {"input_tokens": 11275, "output_tokens": 384, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11659}}}} +{"t_ms": 4057, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0c498203-4202-446f-8df1-dfa8f16cb19d", "step_index": 2, "state": "ACTIVE", "step_type": "tool", "tool_name": "view_file", "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/owned/attachment.png"}}}}} +{"t_ms": 4057, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0c498203-4202-446f-8df1-dfa8f16cb19d", "step_index": 2, "state": "DONE", "step_type": "tool", "tool_name": "view_file", "duration_seconds": 0.003012462, "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/owned/attachment.png"}}}}} +{"t_ms": 7530, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0c498203-4202-446f-8df1-dfa8f16cb19d", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "Based on the image at [/"}}} +{"t_ms": 7555, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0c498203-4202-446f-8df1-dfa8f16cb19d", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "tmp/agy-p2/o"}}} +{"t_ms": 7581, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0c498203-4202-446f-8df1-dfa8f16cb19d", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "wned/attachment.png](fil"}}} +{"t_ms": 7606, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0c498203-4202-446f-8df1-dfa8f16cb19d", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "e:///tmp/agy"}}} +{"t_ms": 7631, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0c498203-4202-446f-8df1-dfa8f16cb19d", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "-p2/owned/attachment.png"}}} +{"t_ms": 7657, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0c498203-4202-446f-8df1-dfa8f16cb19d", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "), here are "}}} +{"t_ms": 7682, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0c498203-4202-446f-8df1-dfa8f16cb19d", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "the visible shapes and colors:\n\n- **Red Triangl"}}} +{"t_ms": 7708, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0c498203-4202-446f-8df1-dfa8f16cb19d", "step_index": 3, "state": "DONE", "step_type": "agent_response", "text_delta": "e**: A solid red triangle positioned on the left side, pointing upwards.\n- **Blue Circle**: A solid blue circle positioned on the lower right side.\n- **Background**: Solid white.\n", "duration_seconds": 3.629267516, "usage": {"input_tokens": 13749, "output_tokens": 170, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 13919}}}} +{"t_ms": 7708, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "0c498203-4202-446f-8df1-dfa8f16cb19d", "status": "SUCCESS", "response": "Based on the image at [/owned/attachment.png](file:///owned/attachment.png), here are the visible shapes and colors:\n\n- **Red Triangle**: A solid red triangle positioned on the left side, pointing upwards.\n- **Blue Circle**: A solid blue circle positioned on the lower right side.\n- **Background**: Solid white.\n", "duration_seconds": 7.367115828, "num_turns": 1, "usage": {"input_tokens": 25024, "output_tokens": 554, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 25578}}}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-add-http.capture.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-add-http.capture.ndjson new file mode 100644 index 0000000000..c04b13268d --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-add-http.capture.ndjson @@ -0,0 +1,3 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "mcp", "add", "phase0", "http://127.0.0.1:/mcp"], "cwd": "", "home": "", "settings": null, "env": {}}} +{"t_ms": 68, "stream": "capture", "data": {"written_file": "/.gemini/config/mcp_config.json", "content": {"mcpServers": {"phase0": {"disabled": false, "serverUrl": "http://127.0.0.1:/mcp"}}}}} +{"t_ms": 69, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-add-http.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-add-http.stderr.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/mcp-add-http.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-add-http.stdin.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/mcp-add-http.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-add-http.stdout.ndjson new file mode 100644 index 0000000000..05d385caa8 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-add-http.stdout.ndjson @@ -0,0 +1 @@ +{"t_ms": 63, "stream": "stdout", "data": "Added MCP server \"phase0\" (http)"} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-add.capture.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-add.capture.ndjson new file mode 100644 index 0000000000..d115585bec --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-add.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "mcp", "add", "phase0", "--url", "http://127.0.0.1:/mcp"], "cwd": "", "home": "", "settings": null, "env": {}}} +{"t_ms": 82, "stream": "capture", "data": {"exit_code": 1}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-add.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-add.stderr.ndjson new file mode 100644 index 0000000000..81181df217 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-add.stderr.ndjson @@ -0,0 +1 @@ +{"t_ms": 76, "stream": "stderr", "data": "Error: invalid command \"--url\": flags must come before the server name (usage: mcp add [flags] [args...])"} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-add.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-add.stdin.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/mcp-add.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-add.stdout.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/mcp-agent-definition.capture.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-agent-definition.capture.ndjson new file mode 100644 index 0000000000..0885d233a5 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-agent-definition.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", "/owned", "--add-dir", "", "--agent", "phase2", "--dangerously-skip-permissions"], "cwd": "", "home": "/mcp-agent-definition-home", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 6091, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-agent-definition.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-agent-definition.stderr.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/mcp-agent-definition.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-agent-definition.stdin.ndjson new file mode 100644 index 0000000000..8daf16fd52 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-agent-definition.stdin.ndjson @@ -0,0 +1 @@ +{"t_ms": 311, "stream": "stdin", "data": {"event": "user", "message": {"content": "Call the phase2 MCP echo tool with text PHASE2_DISCOVERY. Then use run_command pwd and report the workspace directory. Do not choose a different directory."}}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-agent-definition.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-agent-definition.stdout.ndjson new file mode 100644 index 0000000000..854e8701ce --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-agent-definition.stdout.ndjson @@ -0,0 +1,15 @@ +{"t_ms": 311, "stream": "stdout", "data": {"event": "init", "conversation_id": "7240f716-f2bd-4641-b210-a4a282a756f7", "init": {"cwd": "", "agent": "phase2", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "always-proceed"}}} +{"t_ms": 340, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7240f716-f2bd-4641-b210-a4a282a756f7", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 4006, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7240f716-f2bd-4641-b210-a4a282a756f7", "step_index": 1, "state": "DONE", "step_type": "agent_response", "duration_seconds": 3.658926744, "usage": {"input_tokens": 11286, "output_tokens": 361, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11647}}}} +{"t_ms": 4006, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7240f716-f2bd-4641-b210-a4a282a756f7", "step_index": 2, "state": "ACTIVE", "step_type": "tool", "tool_name": "run_command", "tool_info": {"name": "run_command", "parameters": {"CommandLine": "pwd"}}}}} +{"t_ms": 4006, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7240f716-f2bd-4641-b210-a4a282a756f7", "step_index": 2, "state": "DONE", "step_type": "tool", "tool_name": "run_command", "duration_seconds": 0.008819631, "tool_info": {"name": "run_command", "parameters": {"CommandLine": "pwd"}, "output": "/owned\r\n"}}}} +{"t_ms": 5473, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7240f716-f2bd-4641-b210-a4a282a756f7", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "The "}}} +{"t_ms": 5603, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7240f716-f2bd-4641-b210-a4a282a756f7", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "`phase2 MCP echo tool` is not availa"}}} +{"t_ms": 5626, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7240f716-f2bd-4641-b210-a4a282a756f7", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ble in the tool definiti"}}} +{"t_ms": 5652, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7240f716-f2bd-4641-b210-a4a282a756f7", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ons provided"}}} +{"t_ms": 5678, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7240f716-f2bd-4641-b210-a4a282a756f7", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " in this"}}} +{"t_ms": 5856, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7240f716-f2bd-4641-b210-a4a282a756f7", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " environment.\n\nRunning `"}}} +{"t_ms": 5881, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7240f716-f2bd-4641-b210-a4a282a756f7", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "pwd` in the workspace re"}}} +{"t_ms": 5907, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7240f716-f2bd-4641-b210-a4a282a756f7", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ports the directory:\n`/t"}}} +{"t_ms": 5932, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7240f716-f2bd-4641-b210-a4a282a756f7", "step_index": 3, "state": "DONE", "step_type": "agent_response", "text_delta": "mp/agy-p2/owned`\n", "duration_seconds": 1.928095705, "usage": {"input_tokens": 11417, "output_tokens": 43, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11460}}}} +{"t_ms": 5932, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "7240f716-f2bd-4641-b210-a4a282a756f7", "status": "SUCCESS", "response": "The `phase2 MCP echo tool` is not available in the tool definitions provided in this environment.\n\nRunning `pwd` in the workspace reports the directory:\n`/owned`\n", "duration_seconds": 5.61440038, "num_turns": 1, "usage": {"input_tokens": 22703, "output_tokens": 404, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 23107}}}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-agent-discovered.capture.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-agent-discovered.capture.ndjson new file mode 100644 index 0000000000..c5d6ddd6fd --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-agent-discovered.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", "/owned", "--add-dir", "", "--agent", "phase2", "--dangerously-skip-permissions"], "cwd": "", "home": "/mcp-agent-discovered-home", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 5524, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-agent-discovered.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-agent-discovered.stderr.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/mcp-agent-discovered.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-agent-discovered.stdin.ndjson new file mode 100644 index 0000000000..d8cfdf92de --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-agent-discovered.stdin.ndjson @@ -0,0 +1 @@ +{"t_ms": 324, "stream": "stdin", "data": {"event": "user", "message": {"content": "Call the phase2 MCP echo tool with text PHASE2_DISCOVERY. Then use run_command pwd and report the workspace directory. Do not choose a different directory."}}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-agent-discovered.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-agent-discovered.stdout.ndjson new file mode 100644 index 0000000000..a275715465 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-agent-discovered.stdout.ndjson @@ -0,0 +1,19 @@ +{"t_ms": 324, "stream": "stdout", "data": {"event": "init", "conversation_id": "b4d0a25b-bb0a-4495-84af-247f689ba6ad", "init": {"cwd": "", "agent": "phase2", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "always-proceed"}}} +{"t_ms": 356, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b4d0a25b-bb0a-4495-84af-247f689ba6ad", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 3842, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b4d0a25b-bb0a-4495-84af-247f689ba6ad", "step_index": 1, "state": "DONE", "step_type": "agent_response", "duration_seconds": 3.487178538, "usage": {"input_tokens": 11279, "output_tokens": 433, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11712}}}} +{"t_ms": 3842, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b4d0a25b-bb0a-4495-84af-247f689ba6ad", "step_index": 2, "state": "ACTIVE", "step_type": "tool", "tool_name": "run_command", "tool_info": {"name": "run_command", "parameters": {"CommandLine": "pwd"}}}}} +{"t_ms": 3867, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b4d0a25b-bb0a-4495-84af-247f689ba6ad", "step_index": 2, "state": "DONE", "step_type": "tool", "tool_name": "run_command", "duration_seconds": 0.008305961, "tool_info": {"name": "run_command", "parameters": {"CommandLine": "pwd"}, "output": "/owned\r\n"}}}} +{"t_ms": 5050, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b4d0a25b-bb0a-4495-84af-247f689ba6ad", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "1."}}} +{"t_ms": 5177, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b4d0a25b-bb0a-4495-84af-247f689ba6ad", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " **MCP Echo "}}} +{"t_ms": 5203, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b4d0a25b-bb0a-4495-84af-247f689ba6ad", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "Tool**: There is no MCP "}}} +{"t_ms": 5228, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b4d0a25b-bb0a-4495-84af-247f689ba6ad", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "`echo` tool "}}} +{"t_ms": 5254, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b4d0a25b-bb0a-4495-84af-247f689ba6ad", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "registered or available "}}} +{"t_ms": 5279, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b4d0a25b-bb0a-4495-84af-247f689ba6ad", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "in the current environme"}}} +{"t_ms": 5304, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b4d0a25b-bb0a-4495-84af-247f689ba6ad", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "nt/tool definit"}}} +{"t_ms": 5330, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b4d0a25b-bb0a-4495-84af-247f689ba6ad", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ions.\n2. **Workspace Dir"}}} +{"t_ms": 5355, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b4d0a25b-bb0a-4495-84af-247f689ba6ad", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ectory**: Ra"}}} +{"t_ms": 5381, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b4d0a25b-bb0a-4495-84af-247f689ba6ad", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "n `pwd` in the current w"}}} +{"t_ms": 5406, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b4d0a25b-bb0a-4495-84af-247f689ba6ad", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "orkspace directory and o"}}} +{"t_ms": 5432, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b4d0a25b-bb0a-4495-84af-247f689ba6ad", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "btained:\n ```\n /tmp/"}}} +{"t_ms": 5457, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b4d0a25b-bb0a-4495-84af-247f689ba6ad", "step_index": 3, "state": "DONE", "step_type": "agent_response", "text_delta": "agy-p2/owned\n ```\n", "duration_seconds": 1.60141087, "usage": {"input_tokens": 11407, "output_tokens": 62, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11469}}}} +{"t_ms": 5457, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "b4d0a25b-bb0a-4495-84af-247f689ba6ad", "status": "SUCCESS", "response": "1. **MCP Echo Tool**: There is no MCP `echo` tool registered or available in the current environment/tool definitions.\n2. **Workspace Directory**: Ran `pwd` in the current workspace directory and obtained:\n ```\n /owned\n ```\n", "duration_seconds": 5.118791831, "num_turns": 1, "usage": {"input_tokens": 22686, "output_tokens": 495, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 23181}}}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-agent-file.capture.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-agent-file.capture.ndjson new file mode 100644 index 0000000000..57d8da6b18 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-agent-file.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--disable-slash-commands", "--dangerously-skip-permissions", "--add-dir", "", "--agent", "/owned/agent.md"], "cwd": "", "home": "/mcp-agent-file-home", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 12172, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-agent-file.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-agent-file.stderr.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/mcp-agent-file.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-agent-file.stdin.ndjson new file mode 100644 index 0000000000..21feb61a76 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-agent-file.stdin.ndjson @@ -0,0 +1 @@ +{"t_ms": 305, "stream": "stdin", "data": {"event": "user", "message": {"content": "First call the phase2 MCP echo tool with text PHASE2_DISCOVERY. If unavailable say MCP_UNAVAILABLE. Then use shell pwd, read input.txt and write output.txt containing PHASE2_WRITE. Use the workspace default directory, without me supplying absolute paths. Report the exact paths used."}}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-agent-file.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-agent-file.stdout.ndjson new file mode 100644 index 0000000000..3d172dbe90 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-agent-file.stdout.ndjson @@ -0,0 +1,39 @@ +{"t_ms": 305, "stream": "stdout", "data": {"event": "init", "conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "init": {"cwd": "", "agent": "/owned/agent.md", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "always-proceed"}}} +{"t_ms": 334, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 4780, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 1, "state": "DONE", "step_type": "agent_response", "duration_seconds": 4.446916168, "usage": {"input_tokens": 11298, "output_tokens": 523, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11821}}}} +{"t_ms": 4780, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 2, "state": "ACTIVE", "step_type": "tool", "tool_name": "run_command", "tool_info": {"name": "run_command", "parameters": {"CommandLine": "pwd"}}}}} +{"t_ms": 4780, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 2, "state": "DONE", "step_type": "tool", "tool_name": "run_command", "duration_seconds": 0.007696421, "tool_info": {"name": "run_command", "parameters": {"CommandLine": "pwd"}, "output": "\r\n"}}}} +{"t_ms": 6843, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 3, "state": "DONE", "step_type": "agent_response", "duration_seconds": 2.062108191, "usage": {"input_tokens": 11436, "output_tokens": 47, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11483}}}} +{"t_ms": 6843, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 4, "state": "ACTIVE", "step_type": "tool", "tool_name": "view_file", "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/input.txt"}}}}} +{"t_ms": 6869, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 4, "state": "DONE", "step_type": "tool", "tool_name": "view_file", "duration_seconds": 0.002313647, "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/input.txt"}, "output": "1 lines, 23 bytes"}}}} +{"t_ms": 8781, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 5, "state": "DONE", "step_type": "agent_response", "duration_seconds": 1.919162211, "usage": {"input_tokens": 11654, "output_tokens": 79, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11733}}}} +{"t_ms": 8781, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 6, "state": "ACTIVE", "step_type": "tool", "tool_name": "write_to_file", "tool_info": {"name": "write_to_file", "parameters": {"TargetFile": "/output.txt"}}}}} +{"t_ms": 8781, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 6, "state": "DONE", "step_type": "tool", "tool_name": "write_to_file", "duration_seconds": 0.003384418, "tool_info": {"name": "write_to_file", "parameters": {"TargetFile": "/output.txt"}}}}} +{"t_ms": 11046, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "MCP_"}}} +{"t_ms": 11224, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "UNAVAILABLE\n\n### Execution Summary\n\n"}}} +{"t_ms": 11249, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "1. **MCP Ech"}}} +{"t_ms": 11275, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "o Tool Check**:\n"}}} +{"t_ms": 11402, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " - Status: MCP echo to"}}} +{"t_ms": 11427, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ol was not a"}}} +{"t_ms": 11452, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "vailable in the active e"}}} +{"t_ms": 11478, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "nvironment tools (`MCP_U"}}} +{"t_ms": 11504, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "NAVAILABLE`).\n\n2. **Work"}}} +{"t_ms": 11529, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ing Directory (`pwd`)**:"}}} +{"t_ms": 11554, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "\n - Output"}}} +{"t_ms": 11580, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": ": `"}}} +{"t_ms": 11605, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "`\n\n3. **Read File**:"}}} +{"t_ms": 11630, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "\n - Path: "}}} +{"t_ms": 11656, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "[input.txt](file:///tmp/"}}} +{"t_ms": 11681, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "agy-p2/works"}}} +{"t_ms": 11706, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "pace/"}}} +{"t_ms": 11732, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "input.txt)\n "}}} +{"t_ms": 11757, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " - Content: `PHASE2_WOR"}}} +{"t_ms": 11783, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "KSPACE_MARKER`\n\n4. **Wri"}}} +{"t_ms": 11808, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "tten"}}} +{"t_ms": 11859, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " File**:\n "}}} +{"t_ms": 11885, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "- Path: [output.txt](fil"}}} +{"t_ms": 11910, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "e:///workspac"}}} +{"t_ms": 11936, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "e/output.txt)\n - C"}}} +{"t_ms": 11961, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ontent: `PHASE2_WRITE`"}}} +{"t_ms": 12037, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 3.264948634, "usage": {"input_tokens": 11821, "output_tokens": 155, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11976}}}} +{"t_ms": 12063, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "status": "SUCCESS", "response": "MCP_UNAVAILABLE\n\n### Execution Summary\n\n1. **MCP Echo Tool Check**:\n - Status: MCP echo tool was not available in the active environment tools (`MCP_UNAVAILABLE`).\n\n2. **Working Directory (`pwd`)**:\n - Output: ``\n\n3. **Read File**:\n - Path: [input.txt](file:///input.txt)\n - Content: `PHASE2_WORKSPACE_MARKER`\n\n4. **Written File**:\n - Path: [output.txt](file:///output.txt)\n - Content: `PHASE2_WRITE`\n", "duration_seconds": 11.727995605, "num_turns": 1, "usage": {"input_tokens": 46209, "output_tokens": 804, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 47013}}}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-global.capture.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-global.capture.ndjson new file mode 100644 index 0000000000..452d5e7036 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-global.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", "", "--disable-slash-commands", "--dangerously-skip-permissions"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 9237, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-global.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-global.stderr.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/mcp-global.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-global.stdin.ndjson new file mode 100644 index 0000000000..cf779b2f3b --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-global.stdin.ndjson @@ -0,0 +1 @@ +{"t_ms": 300, "stream": "stdin", "data": {"event": "user", "message": {"content": "Call the phase0 MCP echo tool with text PHASE0_MCP. Do not use shell. If unavailable say UNAVAILABLE."}}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-global.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-global.stdout.ndjson new file mode 100644 index 0000000000..06b16f0005 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-global.stdout.ndjson @@ -0,0 +1,14 @@ +{"t_ms": 300, "stream": "stdout", "data": {"event": "init", "conversation_id": "e21f27e1-de55-4f15-a93f-9f70d726915e", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "always-proceed"}}} +{"t_ms": 328, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e21f27e1-de55-4f15-a93f-9f70d726915e", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 3784, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e21f27e1-de55-4f15-a93f-9f70d726915e", "step_index": 1, "state": "DONE", "step_type": "agent_response", "duration_seconds": 3.433302844, "usage": {"input_tokens": 11924, "output_tokens": 406, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12330}}}} +{"t_ms": 3784, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e21f27e1-de55-4f15-a93f-9f70d726915e", "step_index": 2, "state": "ACTIVE", "step_type": "tool", "tool_name": "view_file", "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/.gemini/antigravity-cli/mcp/phase0/echo.json"}}}}} +{"t_ms": 3784, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e21f27e1-de55-4f15-a93f-9f70d726915e", "step_index": 2, "state": "DONE", "step_type": "tool", "tool_name": "view_file", "duration_seconds": 0.003012222, "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/.gemini/antigravity-cli/mcp/phase0/echo.json"}, "output": "1 lines, 184 bytes"}}}} +{"t_ms": 5749, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e21f27e1-de55-4f15-a93f-9f70d726915e", "step_index": 3, "state": "DONE", "step_type": "agent_response", "duration_seconds": 1.966909166, "usage": {"input_tokens": 12221, "output_tokens": 63, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12284}}}} +{"t_ms": 5749, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e21f27e1-de55-4f15-a93f-9f70d726915e", "step_index": 4, "state": "ACTIVE", "step_type": "tool", "tool_name": "call_mcp_tool", "tool_info": {"name": "call_mcp_tool", "parameters": {"Arguments": "{\"text\": \"PHASE0_MCP\"}", "ServerName": "phase0", "ToolName": "echo"}}}}} +{"t_ms": 5749, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e21f27e1-de55-4f15-a93f-9f70d726915e", "step_index": 4, "state": "ERROR", "step_type": "tool", "tool_name": "call_mcp_tool", "duration_seconds": 0.004579062, "tool_info": {"name": "call_mcp_tool", "parameters": {"Arguments": "{\"text\": \"PHASE0_MCP\"}", "ServerName": "phase0", "ToolName": "echo"}, "error": {"type": "TOOL_ERROR", "message": "calling \"tools/call\": [\n {\n \"expected\": \"record\",\n \"code\": \"invalid_type\",\n \"path\": [\n \"params\",\n \"arguments\"\n ],\n \"message\": \"Invalid input: expected record, received string\"\n }\n]"}}}}} +{"t_ms": 7536, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e21f27e1-de55-4f15-a93f-9f70d726915e", "step_index": 5, "state": "DONE", "step_type": "agent_response", "duration_seconds": 1.781658824, "usage": {"input_tokens": 12407, "output_tokens": 60, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12467}}}} +{"t_ms": 7536, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e21f27e1-de55-4f15-a93f-9f70d726915e", "step_index": 6, "state": "ACTIVE", "step_type": "tool", "tool_name": "call_mcp_tool", "tool_info": {"name": "call_mcp_tool", "parameters": {"Arguments": "{\"text\":\"PHASE0_MCP\"}", "ServerName": "phase0", "ToolName": "echo"}}}}} +{"t_ms": 7536, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e21f27e1-de55-4f15-a93f-9f70d726915e", "step_index": 6, "state": "ERROR", "step_type": "tool", "tool_name": "call_mcp_tool", "duration_seconds": 0.003570676, "tool_info": {"name": "call_mcp_tool", "parameters": {"Arguments": "{\"text\":\"PHASE0_MCP\"}", "ServerName": "phase0", "ToolName": "echo"}, "error": {"type": "TOOL_ERROR", "message": "calling \"tools/call\": [\n {\n \"expected\": \"record\",\n \"code\": \"invalid_type\",\n \"path\": [\n \"params\",\n \"arguments\"\n ],\n \"message\": \"Invalid input: expected record, received string\"\n }\n]"}}}}} +{"t_ms": 8744, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e21f27e1-de55-4f15-a93f-9f70d726915e", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "UNAVAILABLE"}}} +{"t_ms": 9073, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e21f27e1-de55-4f15-a93f-9f70d726915e", "step_index": 7, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 1.522542972, "usage": {"input_tokens": 12590, "output_tokens": 2, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12592}}}} +{"t_ms": 9073, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "e21f27e1-de55-4f15-a93f-9f70d726915e", "status": "SUCCESS", "response": "UNAVAILABLE\n", "duration_seconds": 8.74880144, "num_turns": 1, "usage": {"input_tokens": 49142, "output_tokens": 531, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 49673}}}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-help.capture.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-help.capture.ndjson new file mode 100644 index 0000000000..64ea60355d --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-help.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "mcp", "add", "--help"], "cwd": "", "home": "", "settings": null, "env": {}}} +{"t_ms": 75, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-help.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-help.stderr.ndjson new file mode 100644 index 0000000000..93f2f5abb9 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-help.stderr.ndjson @@ -0,0 +1,28 @@ +{"t_ms": 69, "stream": "stderr", "data": "Usage: agy mcp add [flags] [args...]"} +{"t_ms": 69, "stream": "stderr", "data": ""} +{"t_ms": 69, "stream": "stderr", "data": "Add or update an MCP server configuration"} +{"t_ms": 69, "stream": "stderr", "data": ""} +{"t_ms": 69, "stream": "stderr", "data": "Flags:"} +{"t_ms": 69, "stream": "stderr", "data": " -H Short alias for --header (default [])"} +{"t_ms": 69, "stream": "stderr", "data": " -e Short alias for --env (default [])"} +{"t_ms": 69, "stream": "stderr", "data": " --env Environment variable KEY=value (repeatable) (default [])"} +{"t_ms": 69, "stream": "stderr", "data": " -h Show help"} +{"t_ms": 69, "stream": "stderr", "data": " --header HTTP header Key: Value (repeatable) (default [])"} +{"t_ms": 69, "stream": "stderr", "data": " --help Show help"} +{"t_ms": 69, "stream": "stderr", "data": " -t Short alias for --type (default stdio)"} +{"t_ms": 69, "stream": "stderr", "data": " --type Server type: 'stdio' or 'http' (default stdio)"} +{"t_ms": 69, "stream": "stderr", "data": ""} +{"t_ms": 69, "stream": "stderr", "data": "Arguments:"} +{"t_ms": 69, "stream": "stderr", "data": " Unique name for the server."} +{"t_ms": 69, "stream": "stderr", "data": " Executable for a stdio server, or the URL for an http server."} +{"t_ms": 69, "stream": "stderr", "data": " [args...] Arguments passed to the stdio command."} +{"t_ms": 69, "stream": "stderr", "data": ""} +{"t_ms": 69, "stream": "stderr", "data": "Notes:"} +{"t_ms": 69, "stream": "stderr", "data": " Flags must come before ; a flag placed after it is rejected."} +{"t_ms": 69, "stream": "stderr", "data": " http/https URLs are detected automatically, so --type is usually unnecessary."} +{"t_ms": 69, "stream": "stderr", "data": " Use -- before the command to pass a command or args that begin with '-'."} +{"t_ms": 69, "stream": "stderr", "data": ""} +{"t_ms": 69, "stream": "stderr", "data": "Examples:"} +{"t_ms": 69, "stream": "stderr", "data": " agy mcp add fs npx -y /work"} +{"t_ms": 69, "stream": "stderr", "data": " agy mcp add --header \": \" api https://example.com/mcp"} +{"t_ms": 69, "stream": "stderr", "data": " agy mcp add --env = gh -- docker run -i "} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-help.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-help.stdin.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/mcp-help.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-help.stdout.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/mcp-http.requests.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-http.requests.ndjson new file mode 100644 index 0000000000..ebe28a2ddb --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-http.requests.ndjson @@ -0,0 +1,20 @@ +{"stream": "http_request", "data": {"direction": "server_to_mcp", "message": {"jsonrpc": "2.0", "id": 1, "method": "server/discover", "params": {"_meta": {"io.modelcontextprotocol/clientCapabilities": {"elicitation": {"form": {}, "url": {}}, "roots": {"listChanged": true}}, "io.modelcontextprotocol/clientInfo": {"name": "antigravity-client", "version": "v1.0.0"}, "io.modelcontextprotocol/protocolVersion": "2026-07-28"}}}}} +{"stream": "http_request", "data": {"direction": "server_to_mcp", "message": {"jsonrpc": "2.0", "id": 2, "method": "initialize", "params": {"clientInfo": {"name": "antigravity-client", "version": "v1.0.0"}, "protocolVersion": "2025-11-25", "capabilities": {"elicitation": {"form": {}, "url": {}}, "roots": {"listChanged": true}}}}}} +{"stream": "http_request", "data": {"direction": "server_to_mcp", "message": {"jsonrpc": "2.0", "method": "notifications/roots/list_changed", "params": {}}}} +{"stream": "http_request", "data": {"direction": "server_to_mcp", "message": {"jsonrpc": "2.0", "method": "notifications/initialized", "params": {}}}} +{"stream": "http_request", "data": {"direction": "server_to_mcp", "message": {"jsonrpc": "2.0", "id": 3, "method": "tools/list", "params": {}}}} +{"stream": "http_request", "data": {"direction": "server_to_mcp", "message": {"jsonrpc": "2.0", "method": "notifications/roots/list_changed", "params": {}}}} +{"stream": "http_request", "data": {"direction": "server_to_mcp", "message": {"jsonrpc": "2.0", "id": 4, "method": "tools/call", "params": {"_meta": {"antigravity.google/artifacts_dir": "/.gemini/antigravity-cli/brain/e21f27e1-de55-4f15-a93f-9f70d726915e", "antigravity.google/conversation_id": "e21f27e1-de55-4f15-a93f-9f70d726915e", "progressToken": "88c11f68-c1b5-46f7-ae6d-1cb662c4ca0c:4"}, "name": "echo", "arguments": "{\"text\": \"PHASE0_MCP\"}"}}}} +{"stream": "http_request", "data": {"direction": "server_to_mcp", "message": {"jsonrpc": "2.0", "id": 5, "method": "tools/call", "params": {"_meta": {"antigravity.google/artifacts_dir": "/.gemini/antigravity-cli/brain/e21f27e1-de55-4f15-a93f-9f70d726915e", "antigravity.google/conversation_id": "e21f27e1-de55-4f15-a93f-9f70d726915e", "progressToken": "88c11f68-c1b5-46f7-ae6d-1cb662c4ca0c:6"}, "name": "echo", "arguments": "{\"text\":\"PHASE0_MCP\"}"}}}} +{"stream": "http_request", "data": {"direction": "server_to_mcp", "message": {"jsonrpc": "2.0", "id": 1, "method": "server/discover", "params": {"_meta": {"io.modelcontextprotocol/clientCapabilities": {"elicitation": {"form": {}, "url": {}}, "roots": {"listChanged": true}}, "io.modelcontextprotocol/clientInfo": {"name": "antigravity-client", "version": "v1.0.0"}, "io.modelcontextprotocol/protocolVersion": "2026-07-28"}}}}} +{"stream": "http_request", "data": {"direction": "server_to_mcp", "message": {"jsonrpc": "2.0", "id": 2, "method": "initialize", "params": {"clientInfo": {"name": "antigravity-client", "version": "v1.0.0"}, "protocolVersion": "2025-11-25", "capabilities": {"elicitation": {"form": {}, "url": {}}, "roots": {"listChanged": true}}}}}} +{"stream": "http_request", "data": {"direction": "server_to_mcp", "message": {"jsonrpc": "2.0", "method": "notifications/initialized", "params": {}}}} +{"stream": "http_request", "data": {"direction": "server_to_mcp", "message": {"jsonrpc": "2.0", "id": 3, "method": "tools/list", "params": {}}}} +{"stream": "http_request", "data": {"direction": "server_to_mcp", "message": {"jsonrpc": "2.0", "id": 4, "method": "tools/call", "params": {"_meta": {"antigravity.google/artifacts_dir": "/.gemini/antigravity-cli/brain/89273b5a-77cd-4d4b-ac80-4475b2c751d0", "antigravity.google/conversation_id": "89273b5a-77cd-4d4b-ac80-4475b2c751d0", "progressToken": "c009d4b2-d4f6-44f3-9a29-1b0092eb123f:4"}, "name": "echo", "arguments": "{\"text\":\"PHASE0_MCP\"}"}}}} +{"stream": "http_request", "data": {"direction": "server_to_mcp", "message": {"jsonrpc": "2.0", "id": 5, "method": "tools/call", "params": {"_meta": {"antigravity.google/artifacts_dir": "/.gemini/antigravity-cli/brain/89273b5a-77cd-4d4b-ac80-4475b2c751d0", "antigravity.google/conversation_id": "89273b5a-77cd-4d4b-ac80-4475b2c751d0", "progressToken": "c009d4b2-d4f6-44f3-9a29-1b0092eb123f:6"}, "name": "echo", "arguments": "{\"text\": \"PHASE0_MCP\"}"}}}} +{"stream": "http_request", "data": {"direction": "server_to_mcp", "message": {"jsonrpc": "2.0", "id": 1, "method": "server/discover", "params": {"_meta": {"io.modelcontextprotocol/clientCapabilities": {"elicitation": {"form": {}, "url": {}}, "roots": {"listChanged": true}}, "io.modelcontextprotocol/clientInfo": {"name": "antigravity-client", "version": "v1.0.0"}, "io.modelcontextprotocol/protocolVersion": "2026-07-28"}}}}} +{"stream": "http_request", "data": {"direction": "server_to_mcp", "message": {"jsonrpc": "2.0", "id": 2, "method": "initialize", "params": {"clientInfo": {"name": "antigravity-client", "version": "v1.0.0"}, "protocolVersion": "2025-11-25", "capabilities": {"elicitation": {"form": {}, "url": {}}, "roots": {"listChanged": true}}}}}} +{"stream": "http_request", "data": {"direction": "server_to_mcp", "message": {"jsonrpc": "2.0", "method": "notifications/initialized", "params": {}}}} +{"stream": "http_request", "data": {"direction": "server_to_mcp", "message": {"jsonrpc": "2.0", "id": 3, "method": "tools/list", "params": {}}}} +{"stream": "http_request", "data": {"direction": "server_to_mcp", "message": {"jsonrpc": "2.0", "id": 4, "method": "tools/call", "params": {"_meta": {"antigravity.google/artifacts_dir": "/.gemini/antigravity-cli/brain/0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "antigravity.google/conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "progressToken": "d3dee97c-770c-4268-86af-baa39125d7c0:4"}, "name": "echo", "arguments": "{\"text\":\"PHASE0_MCP\"}"}}}} +{"stream": "http_request", "data": {"direction": "server_to_mcp", "message": {"jsonrpc": "2.0", "id": 5, "method": "tools/call", "params": {"_meta": {"antigravity.google/artifacts_dir": "/.gemini/antigravity-cli/brain/0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "antigravity.google/conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "progressToken": "d3dee97c-770c-4268-86af-baa39125d7c0:6"}, "name": "echo", "arguments": "null"}}}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-list.capture.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-list.capture.ndjson new file mode 100644 index 0000000000..a7a6d66835 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-list.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "mcp", "list"], "cwd": "", "home": "", "settings": null, "env": {}}} +{"t_ms": 71, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-list.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-list.stderr.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/mcp-list.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-list.stdin.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/mcp-list.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-list.stdout.ndjson new file mode 100644 index 0000000000..644935d9f8 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-list.stdout.ndjson @@ -0,0 +1 @@ +{"t_ms": 65, "stream": "stdout", "data": "No MCP servers configured."} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-owned-cwd.capture.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-owned-cwd.capture.ndjson new file mode 100644 index 0000000000..ec822f7c19 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-owned-cwd.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--disable-slash-commands", "--dangerously-skip-permissions", "--add-dir", ""], "cwd": "/owned-parent/workspace", "home": "/mcp-owned-cwd-home", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}, "cwd_realpath": "/owned"}} +{"t_ms": 21316, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-owned-cwd.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-owned-cwd.stderr.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/mcp-owned-cwd.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-owned-cwd.stdin.ndjson new file mode 100644 index 0000000000..4606a29d9d --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-owned-cwd.stdin.ndjson @@ -0,0 +1 @@ +{"t_ms": 299, "stream": "stdin", "data": {"event": "user", "message": {"content": "First call the phase2 MCP echo tool with text PHASE2_DISCOVERY. If unavailable say MCP_UNAVAILABLE. Then use shell pwd, read input.txt and write output.txt containing PHASE2_WRITE. Use the workspace default directory, without me supplying absolute paths. Report the exact paths used."}}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-owned-cwd.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-owned-cwd.stdout.ndjson new file mode 100644 index 0000000000..fd49e1756a --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-owned-cwd.stdout.ndjson @@ -0,0 +1,95 @@ +{"t_ms": 299, "stream": "stdout", "data": {"event": "init", "conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "init": {"cwd": "/owned", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "always-proceed"}}} +{"t_ms": 353, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 4289, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 1, "state": "DONE", "step_type": "agent_response", "duration_seconds": 3.923817807, "usage": {"input_tokens": 11986, "output_tokens": 312, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12298}}}} +{"t_ms": 4289, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 2, "state": "ACTIVE", "step_type": "tool", "tool_name": "view_file", "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/mcp-owned-cwd-home/.gemini/antigravity-cli/mcp/phase2/echo.json"}}}}} +{"t_ms": 4289, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 2, "state": "DONE", "step_type": "tool", "tool_name": "view_file", "duration_seconds": 0.003110546, "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/mcp-owned-cwd-home/.gemini/antigravity-cli/mcp/phase2/echo.json"}, "output": "1 lines, 184 bytes"}}}} +{"t_ms": 6028, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 3, "state": "DONE", "step_type": "agent_response", "duration_seconds": 1.727561556, "usage": {"input_tokens": 12292, "output_tokens": 64, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12356}}}} +{"t_ms": 6028, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 4, "state": "ACTIVE", "step_type": "tool", "tool_name": "call_mcp_tool", "tool_info": {"name": "call_mcp_tool", "parameters": {"Arguments": "{\"text\": \"PHASE2_DISCOVERY\"}", "ServerName": "phase2", "ToolName": "echo"}}}}} +{"t_ms": 6028, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 4, "state": "ERROR", "step_type": "tool", "tool_name": "call_mcp_tool", "duration_seconds": 0.003994388, "tool_info": {"name": "call_mcp_tool", "parameters": {"Arguments": "{\"text\": \"PHASE2_DISCOVERY\"}", "ServerName": "phase2", "ToolName": "echo"}, "error": {"type": "TOOL_ERROR", "message": "calling \"tools/call\": [\n {\n \"expected\": \"record\",\n \"code\": \"invalid_type\",\n \"path\": [\n \"params\",\n \"arguments\"\n ],\n \"message\": \"Invalid input: expected record, received string\"\n }\n]"}}}}} +{"t_ms": 7764, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 5, "state": "DONE", "step_type": "agent_response", "duration_seconds": 1.73152143, "usage": {"input_tokens": 12479, "output_tokens": 63, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12542}}}} +{"t_ms": 7764, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 6, "state": "ACTIVE", "step_type": "tool", "tool_name": "call_mcp_tool", "tool_info": {"name": "call_mcp_tool", "parameters": {"Arguments": "{\"text\":\"PHASE2_DISCOVERY\"}", "ServerName": "phase2", "ToolName": "echo"}}}}} +{"t_ms": 7764, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 6, "state": "ERROR", "step_type": "tool", "tool_name": "call_mcp_tool", "duration_seconds": 0.003571568, "tool_info": {"name": "call_mcp_tool", "parameters": {"Arguments": "{\"text\":\"PHASE2_DISCOVERY\"}", "ServerName": "phase2", "ToolName": "echo"}, "error": {"type": "TOOL_ERROR", "message": "calling \"tools/call\": [\n {\n \"expected\": \"record\",\n \"code\": \"invalid_type\",\n \"path\": [\n \"params\",\n \"arguments\"\n ],\n \"message\": \"Invalid input: expected record, received string\"\n }\n]"}}}}} +{"t_ms": 9626, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 7, "state": "DONE", "step_type": "agent_response", "duration_seconds": 1.8600456250000001, "usage": {"input_tokens": 12665, "output_tokens": 70, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12735}}}} +{"t_ms": 9626, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 8, "state": "ACTIVE", "step_type": "tool", "tool_name": "run_command", "tool_info": {"name": "run_command", "parameters": {"CommandLine": "pwd"}}}}} +{"t_ms": 9626, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 8, "state": "DONE", "step_type": "tool", "tool_name": "run_command", "duration_seconds": 0.005713043, "tool_info": {"name": "run_command", "parameters": {"CommandLine": "pwd"}, "output": "/owned\r\n"}}}} +{"t_ms": 11152, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 9, "state": "DONE", "step_type": "agent_response", "duration_seconds": 1.5154668610000002, "usage": {"input_tokens": 12800, "output_tokens": 47, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12847}}}} +{"t_ms": 11152, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 10, "state": "ACTIVE", "step_type": "tool", "tool_name": "view_file", "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/owned/input.txt"}}}}} +{"t_ms": 11152, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 10, "state": "ERROR", "step_type": "tool", "tool_name": "view_file", "duration_seconds": 0.002729213, "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/owned/input.txt"}, "error": {"type": "TOOL_ERROR", "message": "declaring permissions: cortex tool view_file: convert tool call for permissions: model output error: invalid tool call error (invalid_args) failed to read file: stat /owned/input.txt: no such file or directory"}}}}} +{"t_ms": 13856, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 11, "state": "DONE", "step_type": "agent_response", "duration_seconds": 2.6989989100000003, "usage": {"input_tokens": 12961, "output_tokens": 48, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 13009}}}} +{"t_ms": 13856, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 12, "state": "ACTIVE", "step_type": "tool", "tool_name": "view_file", "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/input.txt"}}}}} +{"t_ms": 13856, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 12, "state": "DONE", "step_type": "tool", "tool_name": "view_file", "duration_seconds": 0.002359752, "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/input.txt"}, "output": "1 lines, 23 bytes"}}}} +{"t_ms": 15999, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 13, "state": "DONE", "step_type": "agent_response", "duration_seconds": 2.144108405, "usage": {"input_tokens": 13180, "output_tokens": 78, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 13258}}}} +{"t_ms": 15999, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 14, "state": "ACTIVE", "step_type": "tool", "tool_name": "write_to_file", "tool_info": {"name": "write_to_file", "parameters": {"TargetFile": "/owned/output.txt"}}}}} +{"t_ms": 15999, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 14, "state": "DONE", "step_type": "tool", "tool_name": "write_to_file", "duration_seconds": 0.004225731, "tool_info": {"name": "write_to_file", "parameters": {"TargetFile": "/owned/output.txt"}}}}} +{"t_ms": 17563, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 15, "state": "DONE", "step_type": "agent_response", "duration_seconds": 1.5609718259999998, "usage": {"input_tokens": 13346, "output_tokens": 79, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 13425}}}} +{"t_ms": 17563, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 16, "state": "ACTIVE", "step_type": "tool", "tool_name": "write_to_file", "tool_info": {"name": "write_to_file", "parameters": {"TargetFile": "/output.txt"}}}}} +{"t_ms": 17563, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 16, "state": "DONE", "step_type": "tool", "tool_name": "write_to_file", "duration_seconds": 0.003200805, "tool_info": {"name": "write_to_file", "parameters": {"TargetFile": "/output.txt"}}}}} +{"t_ms": 18830, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "### Phase"}}} +{"t_ms": 19110, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " 2 MCP Tool Call\nThe `ph"}}} +{"t_ms": 19134, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ase2` MCP server was cal"}}} +{"t_ms": 19160, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "led with `ec"}}} +{"t_ms": 19185, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ho`, but tool execution "}}} +{"t_ms": 19211, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "returned a p"}}} +{"t_ms": 19237, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "arameter parsing error (`invalid_typ"}}} +{"t_ms": 19262, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "e: expected record, rece"}}} +{"t_ms": 19288, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ived string`"}}} +{"t_ms": 19314, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " when passing arguments)"}}} +{"t_ms": 19339, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": ". Therefore:"}}} +{"t_ms": 19365, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " **MCP_UNAVAILABLE** (or"}}} +{"t_ms": 19390, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " encounterin"}}} +{"t_ms": 19416, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "g invocation schema incompatibilitie"}}} +{"t_ms": 19441, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "s).\n\n---\n\n### Shell `pwd"}}} +{"t_ms": 19466, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "`\nExecuted `"}}} +{"t_ms": 19492, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "pwd` in the default comm"}}} +{"t_ms": 19517, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "and working "}}} +{"t_ms": 19544, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "directory:\n- **Default CWD:** `/tmp/"}}} +{"t_ms": 19570, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "agy-p2/owned`\n\n---\n\n### "}}} +{"t_ms": 19595, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "File Operati"}}} +{"t_ms": 19621, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ons\n1. **Read `input.txt"}}} +{"t_ms": 19646, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "`:**\n - Attempted: [`/o"}}} +{"t_ms": 19672, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "wned/input.t"}}} +{"t_ms": 19698, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "xt`](file:///"}}} +{"t_ms": 19723, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "owned/"}}} +{"t_ms": 19774, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "input.txt) (not present)\n - Read f"}}} +{"t_ms": 19799, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "rom:"}}} +{"t_ms": 19879, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " [`"}}} +{"t_ms": 19905, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "/input.txt`](file:///tmp"}}} +{"t_ms": 19930, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "/agy-p2/work"}}} +{"t_ms": 19956, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "space/input.txt)\n - Content: `PHAS"}}} +{"t_ms": 19981, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "E2_WORKSPACE"}}} +{"t_ms": 20006, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "_MARKER`\n\n2. **Wrote `ou"}}} +{"t_ms": 20032, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "tput.txt`:**\n"}}} +{"t_ms": 20083, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " - Written"}}} +{"t_ms": 20109, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " to: [`/tmp/"}}} +{"t_ms": 20134, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "agy-p2/owned/output.txt`"}}} +{"t_ms": 20160, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "](file:///tmp/agy-p"}}} +{"t_ms": 20185, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "2/owned/output.txt) (def"}}} +{"t_ms": 20211, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ault CWD)\n - Also writ"}}} +{"t_ms": 20237, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ten to:"}}} +{"t_ms": 20364, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " [`/tmp/agy-"}}} +{"t_ms": 20389, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "p2/workspace/output.txt`"}}} +{"t_ms": 20415, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "](file:///wor"}}} +{"t_ms": 20440, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "kspace/outpu"}}} +{"t_ms": 20465, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "t.txt) ("}}} +{"t_ms": 20491, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "active workspace directo"}}} +{"t_ms": 20517, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ry)\n - Content: `PHASE"}}} +{"t_ms": 20542, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "2_WRITE`\n\n---\n\n### Exact"}}} +{"t_ms": 20568, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " Paths Used\n"}}} +{"t_ms": 20644, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "- **Shell CWD:** `/tmp/a"}}} +{"t_ms": 20671, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "gy-p2/owned`\n- **Input p"}}} +{"t_ms": 20695, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ath:**"}}} +{"t_ms": 20747, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " [`"}}} +{"t_ms": 20773, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "/input.txt`](file:///tmp"}}} +{"t_ms": 20799, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "/agy-p2/workspace/input."}}} +{"t_ms": 20824, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "txt"}}} +{"t_ms": 20850, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": ")\n- **Output path:** [`/"}}} +{"t_ms": 20876, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "tmp/agy-p2/o"}}} +{"t_ms": 20901, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "wned/output.txt`](file:/"}}} +{"t_ms": 20927, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "//"}}} +{"t_ms": 20952, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "tmp/agy-p2/o"}}} +{"t_ms": 20978, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "wned/output.txt) &"}}} +{"t_ms": 21105, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " [`"}}} +{"t_ms": 21131, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "/output.txt`"}}} +{"t_ms": 21156, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "](file:///wor"}}} +{"t_ms": 21181, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "DONE", "step_type": "agent_response", "text_delta": "kspace/output.txt)\n", "duration_seconds": 3.625222904, "usage": {"input_tokens": 13513, "output_tokens": 439, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 13952}}}} +{"t_ms": 21207, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "status": "SUCCESS", "response": "### Phase 2 MCP Tool Call\nThe `phase2` MCP server was called with `echo`, but tool execution returned a parameter parsing error (`invalid_type: expected record, received string` when passing arguments). Therefore: **MCP_UNAVAILABLE** (or encountering invocation schema incompatibilities).\n\n---\n\n### Shell `pwd`\nExecuted `pwd` in the default command working directory:\n- **Default CWD:** `/owned`\n\n---\n\n### File Operations\n1. **Read `input.txt`:**\n - Attempted: [`/owned/input.txt`](file:///owned/input.txt) (not present)\n - Read from: [`/input.txt`](file:///input.txt)\n - Content: `PHASE2_WORKSPACE_MARKER`\n\n2. **Wrote `output.txt`:**\n - Written to: [`/owned/output.txt`](file:///owned/output.txt) (default CWD)\n - Also written to: [`/output.txt`](file:///output.txt) (active workspace directory)\n - Content: `PHASE2_WRITE`\n\n---\n\n### Exact Paths Used\n- **Shell CWD:** `/owned`\n- **Input path:** [`/input.txt`](file:///input.txt)\n- **Output path:** [`/owned/output.txt`](file:///owned/output.txt) & [`/output.txt`](file:///output.txt)\n", "duration_seconds": 20.876486639, "num_turns": 1, "usage": {"input_tokens": 115222, "output_tokens": 1200, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 116422}}}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-owned-first-add-dir.capture.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-owned-first-add-dir.capture.ndjson new file mode 100644 index 0000000000..ea2081bbd9 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-owned-first-add-dir.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--disable-slash-commands", "--dangerously-skip-permissions", "--add-dir", "/owned", "--add-dir", ""], "cwd": "", "home": "/mcp-owned-first-add-dir-home", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 23197, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-owned-first-add-dir.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-owned-first-add-dir.stderr.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/mcp-owned-first-add-dir.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-owned-first-add-dir.stdin.ndjson new file mode 100644 index 0000000000..1cb2727caa --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-owned-first-add-dir.stdin.ndjson @@ -0,0 +1 @@ +{"t_ms": 307, "stream": "stdin", "data": {"event": "user", "message": {"content": "First call the phase2 MCP echo tool with text PHASE2_DISCOVERY. If unavailable say MCP_UNAVAILABLE. Then use shell pwd, read input.txt and write output.txt containing PHASE2_WRITE. Use the workspace default directory, without me supplying absolute paths. Report the exact paths used."}}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-owned-first-add-dir.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-owned-first-add-dir.stdout.ndjson new file mode 100644 index 0000000000..8f9424660b --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-owned-first-add-dir.stdout.ndjson @@ -0,0 +1,60 @@ +{"t_ms": 307, "stream": "stdout", "data": {"event": "init", "conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "always-proceed"}}} +{"t_ms": 373, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 3416, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 1, "state": "DONE", "step_type": "agent_response", "duration_seconds": 3.021318824, "usage": {"input_tokens": 12003, "output_tokens": 294, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12297}}}} +{"t_ms": 3416, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 2, "state": "ACTIVE", "step_type": "tool", "tool_name": "view_file", "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/mcp-owned-first-add-dir-home/.gemini/antigravity-cli/mcp/phase2/echo.json"}}}}} +{"t_ms": 3416, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 2, "state": "DONE", "step_type": "tool", "tool_name": "view_file", "duration_seconds": 0.004082433, "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/mcp-owned-first-add-dir-home/.gemini/antigravity-cli/mcp/phase2/echo.json"}, "output": "1 lines, 184 bytes"}}}} +{"t_ms": 5132, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 3, "state": "DONE", "step_type": "agent_response", "duration_seconds": 1.7174826109999999, "usage": {"input_tokens": 12318, "output_tokens": 64, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12382}}}} +{"t_ms": 5133, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 4, "state": "ACTIVE", "step_type": "tool", "tool_name": "call_mcp_tool", "tool_info": {"name": "call_mcp_tool", "parameters": {"Arguments": "{\"text\":\"PHASE2_DISCOVERY\"}", "ServerName": "phase2", "ToolName": "echo"}}}}} +{"t_ms": 5133, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 4, "state": "ERROR", "step_type": "tool", "tool_name": "call_mcp_tool", "duration_seconds": 0.005855559, "tool_info": {"name": "call_mcp_tool", "parameters": {"Arguments": "{\"text\":\"PHASE2_DISCOVERY\"}", "ServerName": "phase2", "ToolName": "echo"}, "error": {"type": "TOOL_ERROR", "message": "calling \"tools/call\": [\n {\n \"expected\": \"record\",\n \"code\": \"invalid_type\",\n \"path\": [\n \"params\",\n \"arguments\"\n ],\n \"message\": \"Invalid input: expected record, received string\"\n }\n]"}}}}} +{"t_ms": 9981, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 5, "state": "DONE", "step_type": "agent_response", "duration_seconds": 4.850317858, "usage": {"input_tokens": 12505, "output_tokens": 68, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12573}}}} +{"t_ms": 9982, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 6, "state": "ACTIVE", "step_type": "tool", "tool_name": "run_command", "tool_info": {"name": "run_command", "parameters": {"CommandLine": "pwd"}}}}} +{"t_ms": 10007, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 6, "state": "DONE", "step_type": "tool", "tool_name": "run_command", "duration_seconds": 0.005511565, "tool_info": {"name": "run_command", "parameters": {"CommandLine": "pwd"}, "output": "/owned\r\n"}}}} +{"t_ms": 11982, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 7, "state": "DONE", "step_type": "agent_response", "duration_seconds": 1.9862325090000001, "usage": {"input_tokens": 12638, "output_tokens": 47, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12685}}}} +{"t_ms": 11982, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 8, "state": "ACTIVE", "step_type": "tool", "tool_name": "view_file", "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/owned/input.txt"}}}}} +{"t_ms": 11982, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 8, "state": "ERROR", "step_type": "tool", "tool_name": "view_file", "duration_seconds": 0.002185687, "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/owned/input.txt"}, "error": {"type": "TOOL_ERROR", "message": "declaring permissions: cortex tool view_file: convert tool call for permissions: model output error: invalid tool call error (invalid_args) failed to read file: stat /owned/input.txt: no such file or directory"}}}}} +{"t_ms": 13701, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 9, "state": "DONE", "step_type": "agent_response", "duration_seconds": 1.704119444, "usage": {"input_tokens": 12799, "output_tokens": 51, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12850}}}} +{"t_ms": 13701, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 10, "state": "ACTIVE", "step_type": "tool", "tool_name": "view_file", "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/input.txt"}}}}} +{"t_ms": 13701, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 10, "state": "DONE", "step_type": "tool", "tool_name": "view_file", "duration_seconds": 0.003250577, "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/input.txt"}, "output": "1 lines, 23 bytes"}}}} +{"t_ms": 15580, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 11, "state": "DONE", "step_type": "agent_response", "duration_seconds": 1.85800527, "usage": {"input_tokens": 13021, "output_tokens": 82, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 13103}}}} +{"t_ms": 15580, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 12, "state": "ACTIVE", "step_type": "tool", "tool_name": "write_to_file", "tool_info": {"name": "write_to_file", "parameters": {"TargetFile": "/owned/output.txt"}}}}} +{"t_ms": 15580, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 12, "state": "DONE", "step_type": "tool", "tool_name": "write_to_file", "duration_seconds": 0.005710137, "tool_info": {"name": "write_to_file", "parameters": {"TargetFile": "/owned/output.txt"}}}}} +{"t_ms": 17976, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 13, "state": "DONE", "step_type": "agent_response", "duration_seconds": 2.381626267, "usage": {"input_tokens": 13191, "output_tokens": 82, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 13273}}}} +{"t_ms": 17976, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 14, "state": "ACTIVE", "step_type": "tool", "tool_name": "write_to_file", "tool_info": {"name": "write_to_file", "parameters": {"TargetFile": "/output.txt"}}}}} +{"t_ms": 17976, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 14, "state": "DONE", "step_type": "tool", "tool_name": "write_to_file", "duration_seconds": 0.002994941, "tool_info": {"name": "write_to_file", "parameters": {"TargetFile": "/output.txt"}}}}} +{"t_ms": 21806, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "1"}}} +{"t_ms": 21908, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": ". **MCP Tool Call (`phas"}}} +{"t_ms": 21939, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "e2` / `echo`):**\n - At"}}} +{"t_ms": 21971, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "tempt"}}} +{"t_ms": 22074, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ed to invoke"}}} +{"t_ms": 22099, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " `phase2` `echo` with te"}}} +{"t_ms": 22127, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "xt `PHASE2_DISCOVERY`. T"}}} +{"t_ms": 22159, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "he MCP tool returned"}}} +{"t_ms": 22213, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " an interface/parameter "}}} +{"t_ms": 22249, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "error (`Invalid input: e"}}} +{"t_ms": 22268, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "xpected record, received"}}} +{"t_ms": 22293, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " string`), i"}}} +{"t_ms": 22319, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ndicating the tool could not process"}}} +{"t_ms": 22349, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " the call: `"}}} +{"t_ms": 22375, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "MCP_UNAVAILABLE`.\n\n2. **"}}} +{"t_ms": 22402, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "Shell `pwd`:**\n - Defa"}}} +{"t_ms": 22430, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ult working "}}} +{"t_ms": 22458, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "directory: `/tmp/"}}} +{"t_ms": 22509, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "agy-p2/owned`\n\n3. **File"}}} +{"t_ms": 22535, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " Operations "}}} +{"t_ms": 22569, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "& Exact Paths:**\n - **"}}} +{"t_ms": 22594, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "Input File Read:**"}}} +{"t_ms": 22619, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " [input.txt](file:///work"}}} +{"t_ms": 22645, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "space/input."}}} +{"t_ms": 22671, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "txt) (content: `PHASE2_"}}} +{"t_ms": 22726, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "WORKSPACE_MARKER`)\n - "}}} +{"t_ms": 22751, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "**Output Files Written:*"}}} +{"t_ms": 22776, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "*\n - [ou"}}} +{"t_ms": 22807, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "tput.txt](file:///owne"}}} +{"t_ms": 22833, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "d/output.txt) (content: "}}} +{"t_ms": 22859, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "`PHASE2_WRITE`)\n -"}}} +{"t_ms": 22911, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " [output.txt](file:///tm"}}} +{"t_ms": 22937, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "p/agy-p2/workspace/outpu"}}} +{"t_ms": 22964, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "t.txt) (content: `PHASE2"}}} +{"t_ms": 22990, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "_WRITE`)"}}} +{"t_ms": 23043, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 5.067703746, "usage": {"input_tokens": 13361, "output_tokens": 225, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 13586}}}} +{"t_ms": 23043, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "status": "SUCCESS", "response": "1. **MCP Tool Call (`phase2` / `echo`):**\n - Attempted to invoke `phase2` `echo` with text `PHASE2_DISCOVERY`. The MCP tool returned an interface/parameter error (`Invalid input: expected record, received string`), indicating the tool could not process the call: `MCP_UNAVAILABLE`.\n\n2. **Shell `pwd`:**\n - Default working directory: `/owned`\n\n3. **File Operations & Exact Paths:**\n - **Input File Read:** [input.txt](file:///input.txt) (content: `PHASE2_WORKSPACE_MARKER`)\n - **Output Files Written:**\n - [output.txt](file:///owned/output.txt) (content: `PHASE2_WRITE`)\n - [output.txt](file:///output.txt) (content: `PHASE2_WRITE`)\n", "duration_seconds": 22.712812313, "num_turns": 1, "usage": {"input_tokens": 101836, "output_tokens": 913, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 102749}}}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-project-config.capture.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-project-config.capture.ndjson new file mode 100644 index 0000000000..97c48693ea --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-project-config.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", "", "--disable-slash-commands", "--dangerously-skip-permissions"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 3445, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-project-config.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-project-config.stderr.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/mcp-project-config.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-project-config.stdin.ndjson new file mode 100644 index 0000000000..f5fa2b6685 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-project-config.stdin.ndjson @@ -0,0 +1 @@ +{"t_ms": 297, "stream": "stdin", "data": {"event": "user", "message": {"content": "Call the phase0 MCP echo tool with text PHASE0_MCP. Do not use shell. If unavailable say UNAVAILABLE."}}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-project-config.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-project-config.stdout.ndjson new file mode 100644 index 0000000000..c77058cdf8 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-project-config.stdout.ndjson @@ -0,0 +1,5 @@ +{"t_ms": 297, "stream": "stdout", "data": {"event": "init", "conversation_id": "0e78dd7a-7278-49b7-8cd6-68b9c0dfe82b", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "always-proceed"}}} +{"t_ms": 325, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0e78dd7a-7278-49b7-8cd6-68b9c0dfe82b", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 2917, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0e78dd7a-7278-49b7-8cd6-68b9c0dfe82b", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "UNAVAILABLE"}}} +{"t_ms": 3272, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0e78dd7a-7278-49b7-8cd6-68b9c0dfe82b", "step_index": 1, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 2.949354512, "usage": {"input_tokens": 11261, "output_tokens": 222, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11483}}}} +{"t_ms": 3298, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "0e78dd7a-7278-49b7-8cd6-68b9c0dfe82b", "status": "SUCCESS", "response": "UNAVAILABLE\n", "duration_seconds": 2.972410821, "num_turns": 1, "usage": {"input_tokens": 11261, "output_tokens": 222, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11483}}}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-project-isolated.capture.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-project-isolated.capture.ndjson new file mode 100644 index 0000000000..c3f823d332 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-project-isolated.capture.ndjson @@ -0,0 +1,4 @@ +{"t_ms": 0, "stream": "capture", "data": {"setup_file": "/.agents/mcp_config.json", "content": {"mcpServers": {"phase0": {"serverUrl": "http://127.0.0.1:/mcp"}}}, "global_mcp_config_exists": false}} +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", "", "--dangerously-skip-permissions"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 9494, "stream": "capture", "data": {"global_mcp_config_exists": true}} +{"t_ms": 9645, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-project-isolated.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-project-isolated.stderr.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/mcp-project-isolated.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-project-isolated.stdin.ndjson new file mode 100644 index 0000000000..7907cb1235 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-project-isolated.stdin.ndjson @@ -0,0 +1 @@ +{"t_ms": 301, "stream": "stdin", "data": {"event": "user", "message": {"content": "Call the phase0 MCP echo tool with text PHASE0_MCP. If it fails report the exact error. No shell."}}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-project-isolated.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-project-isolated.stdout.ndjson new file mode 100644 index 0000000000..632daa6821 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-project-isolated.stdout.ndjson @@ -0,0 +1,31 @@ +{"t_ms": 301, "stream": "stdout", "data": {"event": "init", "conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "always-proceed"}}} +{"t_ms": 357, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 3586, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 1, "state": "DONE", "step_type": "agent_response", "duration_seconds": 3.20056914, "usage": {"input_tokens": 11979, "output_tokens": 300, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12279}}}} +{"t_ms": 3587, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 2, "state": "ACTIVE", "step_type": "tool", "tool_name": "view_file", "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/.gemini/antigravity-cli/mcp/phase0/echo.json"}}}}} +{"t_ms": 3587, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 2, "state": "DONE", "step_type": "tool", "tool_name": "view_file", "duration_seconds": 0.003339143, "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/.gemini/antigravity-cli/mcp/phase0/echo.json"}, "output": "1 lines, 184 bytes"}}}} +{"t_ms": 5350, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 3, "state": "DONE", "step_type": "agent_response", "duration_seconds": 1.765191975, "usage": {"input_tokens": 12284, "output_tokens": 61, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12345}}}} +{"t_ms": 5350, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 4, "state": "ACTIVE", "step_type": "tool", "tool_name": "call_mcp_tool", "tool_info": {"name": "call_mcp_tool", "parameters": {"Arguments": "{\"text\":\"PHASE0_MCP\"}", "ServerName": "phase0", "ToolName": "echo"}}}}} +{"t_ms": 5350, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 4, "state": "ERROR", "step_type": "tool", "tool_name": "call_mcp_tool", "duration_seconds": 0.004524751, "tool_info": {"name": "call_mcp_tool", "parameters": {"Arguments": "{\"text\":\"PHASE0_MCP\"}", "ServerName": "phase0", "ToolName": "echo"}, "error": {"type": "TOOL_ERROR", "message": "calling \"tools/call\": [\n {\n \"expected\": \"record\",\n \"code\": \"invalid_type\",\n \"path\": [\n \"params\",\n \"arguments\"\n ],\n \"message\": \"Invalid input: expected record, received string\"\n }\n]"}}}}} +{"t_ms": 7088, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 5, "state": "DONE", "step_type": "agent_response", "duration_seconds": 1.72463283, "usage": {"input_tokens": 12468, "output_tokens": 53, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12521}}}} +{"t_ms": 7088, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 6, "state": "ACTIVE", "step_type": "tool", "tool_name": "call_mcp_tool", "tool_info": {"name": "call_mcp_tool", "parameters": {"Arguments": "null", "ServerName": "phase0", "ToolName": "echo"}}}}} +{"t_ms": 7088, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 6, "state": "ERROR", "step_type": "tool", "tool_name": "call_mcp_tool", "duration_seconds": 0.00450856, "tool_info": {"name": "call_mcp_tool", "parameters": {"Arguments": "null", "ServerName": "phase0", "ToolName": "echo"}, "error": {"type": "TOOL_ERROR", "message": "calling \"tools/call\": [\n {\n \"expected\": \"record\",\n \"code\": \"invalid_type\",\n \"path\": [\n \"params\",\n \"arguments\"\n ],\n \"message\": \"Invalid input: expected record, received string\"\n }\n]"}}}}} +{"t_ms": 8903, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "The call"}}} +{"t_ms": 8979, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " to the `pha"}}} +{"t_ms": 9005, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "se0` MCP `echo` tool fai"}}} +{"t_ms": 9031, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "led with the following e"}}} +{"t_ms": 9056, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "xact error:\n"}}} +{"t_ms": 9082, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "\n"}}} +{"t_ms": 9107, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "```json\ncall"}}} +{"t_ms": 9133, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ing \"tools/call\": [\n {\n"}}} +{"t_ms": 9158, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " \"expected\": \"record\""}}} +{"t_ms": 9185, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": ",\n \"code\""}}} +{"t_ms": 9210, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": ": \"invalid_type\",\n \"path\": [\n "}}} +{"t_ms": 9235, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " \"params\""}}} +{"t_ms": 9261, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": ",\n \"arguments\"\n "}}} +{"t_ms": 9286, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "],\n "}}} +{"t_ms": 9312, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "\"message\": \""}}} +{"t_ms": 9338, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "Invalid input: expected "}}} +{"t_ms": 9363, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "record, rece"}}} +{"t_ms": 9389, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ived string\"\n }\n]\n```"}}} +{"t_ms": 9468, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 7, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 2.395342779, "usage": {"input_tokens": 12644, "output_tokens": 93, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12737}}}} +{"t_ms": 9494, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "status": "SUCCESS", "response": "The call to the `phase0` MCP `echo` tool failed with the following exact error:\n\n```json\ncalling \"tools/call\": [\n {\n \"expected\": \"record\",\n \"code\": \"invalid_type\",\n \"path\": [\n \"params\",\n \"arguments\"\n ],\n \"message\": \"Invalid input: expected record, received string\"\n }\n]\n```\n", "duration_seconds": 9.15918287, "num_turns": 1, "usage": {"input_tokens": 49375, "output_tokens": 507, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 49882}}}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-project.capture.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-project.capture.ndjson new file mode 100644 index 0000000000..041597c746 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-project.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", "", "--disable-slash-commands", "--dangerously-skip-permissions"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 30078, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-project.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-project.stderr.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/mcp-project.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-project.stdin.ndjson new file mode 100644 index 0000000000..de5df7cd48 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-project.stdin.ndjson @@ -0,0 +1 @@ +{"t_ms": 286, "stream": "stdin", "data": {"event": "user", "message": {"content": "Call the phase0 MCP echo tool with text PHASE0_MCP. Do not use shell. If unavailable say UNAVAILABLE."}}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-project.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-project.stdout.ndjson new file mode 100644 index 0000000000..323648a229 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-project.stdout.ndjson @@ -0,0 +1,15 @@ +{"t_ms": 285, "stream": "stdout", "data": {"event": "init", "conversation_id": "89273b5a-77cd-4d4b-ac80-4475b2c751d0", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "always-proceed"}}} +{"t_ms": 339, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "89273b5a-77cd-4d4b-ac80-4475b2c751d0", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 4401, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "89273b5a-77cd-4d4b-ac80-4475b2c751d0", "step_index": 1, "state": "DONE", "step_type": "agent_response", "duration_seconds": 4.075516272, "usage": {"input_tokens": 11933, "output_tokens": 393, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12326}}}} +{"t_ms": 4401, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "89273b5a-77cd-4d4b-ac80-4475b2c751d0", "step_index": 2, "state": "ACTIVE", "step_type": "tool", "tool_name": "view_file", "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/.gemini/antigravity-cli/mcp/phase0/echo.json"}}}}} +{"t_ms": 4427, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "89273b5a-77cd-4d4b-ac80-4475b2c751d0", "step_index": 2, "state": "DONE", "step_type": "tool", "tool_name": "view_file", "duration_seconds": 0.003930268, "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/.gemini/antigravity-cli/mcp/phase0/echo.json"}, "output": "1 lines, 184 bytes"}}}} +{"t_ms": 6440, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "89273b5a-77cd-4d4b-ac80-4475b2c751d0", "step_index": 3, "state": "DONE", "step_type": "agent_response", "duration_seconds": 2.013674566, "usage": {"input_tokens": 12234, "output_tokens": 63, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12297}}}} +{"t_ms": 6441, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "89273b5a-77cd-4d4b-ac80-4475b2c751d0", "step_index": 4, "state": "ACTIVE", "step_type": "tool", "tool_name": "call_mcp_tool", "tool_info": {"name": "call_mcp_tool", "parameters": {"Arguments": "{\"text\":\"PHASE0_MCP\"}", "ServerName": "phase0", "ToolName": "echo"}}}}} +{"t_ms": 6441, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "89273b5a-77cd-4d4b-ac80-4475b2c751d0", "step_index": 4, "state": "ERROR", "step_type": "tool", "tool_name": "call_mcp_tool", "duration_seconds": 0.003539628, "tool_info": {"name": "call_mcp_tool", "parameters": {"Arguments": "{\"text\":\"PHASE0_MCP\"}", "ServerName": "phase0", "ToolName": "echo"}, "error": {"type": "TOOL_ERROR", "message": "calling \"tools/call\": [\n {\n \"expected\": \"record\",\n \"code\": \"invalid_type\",\n \"path\": [\n \"params\",\n \"arguments\"\n ],\n \"message\": \"Invalid input: expected record, received string\"\n }\n]"}}}}} +{"t_ms": 9031, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "89273b5a-77cd-4d4b-ac80-4475b2c751d0", "step_index": 5, "state": "DONE", "step_type": "agent_response", "duration_seconds": 2.5843047, "usage": {"input_tokens": 12420, "output_tokens": 64, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12484}}}} +{"t_ms": 9031, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "89273b5a-77cd-4d4b-ac80-4475b2c751d0", "step_index": 6, "state": "ACTIVE", "step_type": "tool", "tool_name": "call_mcp_tool", "tool_info": {"name": "call_mcp_tool", "parameters": {"Arguments": "{\"text\": \"PHASE0_MCP\"}", "ServerName": "phase0", "ToolName": "echo"}}}}} +{"t_ms": 9031, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "89273b5a-77cd-4d4b-ac80-4475b2c751d0", "step_index": 6, "state": "ERROR", "step_type": "tool", "tool_name": "call_mcp_tool", "duration_seconds": 0.002964553, "tool_info": {"name": "call_mcp_tool", "parameters": {"Arguments": "{\"text\": \"PHASE0_MCP\"}", "ServerName": "phase0", "ToolName": "echo"}, "error": {"type": "TOOL_ERROR", "message": "calling \"tools/call\": [\n {\n \"expected\": \"record\",\n \"code\": \"invalid_type\",\n \"path\": [\n \"params\",\n \"arguments\"\n ],\n \"message\": \"Invalid input: expected record, received string\"\n }\n]"}}}}} +{"t_ms": 24041, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "89273b5a-77cd-4d4b-ac80-4475b2c751d0", "step_index": 7, "state": "DONE", "step_type": "error_message", "duration_seconds": 0}}} +{"t_ms": 29423, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "89273b5a-77cd-4d4b-ac80-4475b2c751d0", "step_index": 8, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "UNAVAILABLE"}}} +{"t_ms": 29980, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "89273b5a-77cd-4d4b-ac80-4475b2c751d0", "step_index": 8, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 1.922531311, "usage": {"input_tokens": 12607, "output_tokens": 2, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12609}}}} +{"t_ms": 29980, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "89273b5a-77cd-4d4b-ac80-4475b2c751d0", "status": "ERROR", "response": "UNAVAILABLE\n", "error": "API error (attempt 1): Error 502, Message: , Status: 502 Bad Gateway, Details: []", "duration_seconds": 29.671957293, "num_turns": 1, "usage": {"input_tokens": 49194, "output_tokens": 522, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 49716}}}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-second-add-dir.capture.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-second-add-dir.capture.ndjson new file mode 100644 index 0000000000..d42fd1a128 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-second-add-dir.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--disable-slash-commands", "--dangerously-skip-permissions", "--add-dir", "", "--add-dir", "/owned"], "cwd": "", "home": "/mcp-second-add-dir-home", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 18531, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-second-add-dir.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-second-add-dir.stderr.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/mcp-second-add-dir.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-second-add-dir.stdin.ndjson new file mode 100644 index 0000000000..eeddbbd260 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-second-add-dir.stdin.ndjson @@ -0,0 +1 @@ +{"t_ms": 308, "stream": "stdin", "data": {"event": "user", "message": {"content": "First call the phase2 MCP echo tool with text PHASE2_DISCOVERY. If unavailable say MCP_UNAVAILABLE. Then use shell pwd, read input.txt and write output.txt containing PHASE2_WRITE. Use the workspace default directory, without me supplying absolute paths. Report the exact paths used."}}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-second-add-dir.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-second-add-dir.stdout.ndjson new file mode 100644 index 0000000000..274627ad6b --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-second-add-dir.stdout.ndjson @@ -0,0 +1,71 @@ +{"t_ms": 308, "stream": "stdout", "data": {"event": "init", "conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "always-proceed"}}} +{"t_ms": 386, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 4340, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 1, "state": "DONE", "step_type": "agent_response", "duration_seconds": 3.94876824, "usage": {"input_tokens": 11996, "output_tokens": 440, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12436}}}} +{"t_ms": 4340, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 2, "state": "ACTIVE", "step_type": "tool", "tool_name": "view_file", "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/mcp-second-add-dir-home/.gemini/antigravity-cli/mcp/phase2/echo.json"}}}}} +{"t_ms": 4340, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 2, "state": "DONE", "step_type": "tool", "tool_name": "view_file", "duration_seconds": 0.003707182, "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/mcp-second-add-dir-home/.gemini/antigravity-cli/mcp/phase2/echo.json"}, "output": "1 lines, 184 bytes"}}}} +{"t_ms": 6080, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 3, "state": "DONE", "step_type": "agent_response", "duration_seconds": 1.730381429, "usage": {"input_tokens": 12306, "output_tokens": 64, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12370}}}} +{"t_ms": 6080, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 4, "state": "ACTIVE", "step_type": "tool", "tool_name": "call_mcp_tool", "tool_info": {"name": "call_mcp_tool", "parameters": {"Arguments": "{\"text\":\"PHASE2_DISCOVERY\"}", "ServerName": "phase2", "ToolName": "echo"}}}}} +{"t_ms": 6080, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 4, "state": "ERROR", "step_type": "tool", "tool_name": "call_mcp_tool", "duration_seconds": 0.005173122, "tool_info": {"name": "call_mcp_tool", "parameters": {"Arguments": "{\"text\":\"PHASE2_DISCOVERY\"}", "ServerName": "phase2", "ToolName": "echo"}, "error": {"type": "TOOL_ERROR", "message": "calling \"tools/call\": [\n {\n \"expected\": \"record\",\n \"code\": \"invalid_type\",\n \"path\": [\n \"params\",\n \"arguments\"\n ],\n \"message\": \"Invalid input: expected record, received string\"\n }\n]"}}}}} +{"t_ms": 7982, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 5, "state": "DONE", "step_type": "agent_response", "duration_seconds": 1.901210495, "usage": {"input_tokens": 12493, "output_tokens": 68, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12561}}}} +{"t_ms": 7982, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 6, "state": "ACTIVE", "step_type": "tool", "tool_name": "run_command", "tool_info": {"name": "run_command", "parameters": {"CommandLine": "pwd"}}}}} +{"t_ms": 7982, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 6, "state": "DONE", "step_type": "tool", "tool_name": "run_command", "duration_seconds": 0.006768796, "tool_info": {"name": "run_command", "parameters": {"CommandLine": "pwd"}, "output": "/owned\r\n"}}}} +{"t_ms": 9669, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 7, "state": "DONE", "step_type": "agent_response", "duration_seconds": 1.692085483, "usage": {"input_tokens": 12626, "output_tokens": 48, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12674}}}} +{"t_ms": 9669, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 8, "state": "ACTIVE", "step_type": "tool", "tool_name": "view_file", "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/owned/input.txt"}}}}} +{"t_ms": 9669, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 8, "state": "ERROR", "step_type": "tool", "tool_name": "view_file", "duration_seconds": 0.001462294, "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/owned/input.txt"}, "error": {"type": "TOOL_ERROR", "message": "declaring permissions: cortex tool view_file: convert tool call for permissions: model output error: invalid tool call error (invalid_args) failed to read file: stat /owned/input.txt: no such file or directory"}}}}} +{"t_ms": 11257, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 9, "state": "DONE", "step_type": "agent_response", "duration_seconds": 1.561062696, "usage": {"input_tokens": 12788, "output_tokens": 51, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12839}}}} +{"t_ms": 11257, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 10, "state": "ACTIVE", "step_type": "tool", "tool_name": "view_file", "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/input.txt"}}}}} +{"t_ms": 11257, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 10, "state": "DONE", "step_type": "tool", "tool_name": "view_file", "duration_seconds": 0.002866119, "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/input.txt"}, "output": "1 lines, 23 bytes"}}}} +{"t_ms": 13169, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 11, "state": "DONE", "step_type": "agent_response", "duration_seconds": 1.9176663440000001, "usage": {"input_tokens": 13010, "output_tokens": 84, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 13094}}}} +{"t_ms": 13170, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 12, "state": "ACTIVE", "step_type": "tool", "tool_name": "write_to_file", "tool_info": {"name": "write_to_file", "parameters": {"TargetFile": "/output.txt"}}}}} +{"t_ms": 13170, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 12, "state": "DONE", "step_type": "tool", "tool_name": "write_to_file", "duration_seconds": 0.003297375, "tool_info": {"name": "write_to_file", "parameters": {"TargetFile": "/output.txt"}}}}} +{"t_ms": 15284, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 13, "state": "DONE", "step_type": "agent_response", "duration_seconds": 2.118805123, "usage": {"input_tokens": 13182, "output_tokens": 84, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 13266}}}} +{"t_ms": 15284, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 14, "state": "ACTIVE", "step_type": "tool", "tool_name": "write_to_file", "tool_info": {"name": "write_to_file", "parameters": {"TargetFile": "/owned/output.txt"}}}}} +{"t_ms": 15284, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 14, "state": "DONE", "step_type": "tool", "tool_name": "write_to_file", "duration_seconds": 0.003384669, "tool_info": {"name": "write_to_file", "parameters": {"TargetFile": "/owned/output.txt"}}}}} +{"t_ms": 16695, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "### MCP"}}} +{"t_ms": 16797, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " Tool Execut"}}} +{"t_ms": 16823, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ion\n- Calling the `phase"}}} +{"t_ms": 16848, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "2` MCP `echo` tool faile"}}} +{"t_ms": 16874, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "d with an"}}} +{"t_ms": 17102, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " argument validation error from the "}}} +{"t_ms": 17128, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "server schem"}}} +{"t_ms": 17153, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "a interface:\n `MCP_UNAV"}}} +{"t_ms": 17178, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "AILABLE` (er"}}} +{"t_ms": 17204, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ror: `Invalid input: expected record"}}} +{"t_ms": 17229, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": ", received s"}}} +{"t_ms": 17255, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "tring`)\n\n---\n\n### Shell "}}} +{"t_ms": 17281, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "`pwd` & Workspace File O"}}} +{"t_ms": 17306, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "perations\n- "}}} +{"t_ms": 17331, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "**Current Working Directory (`pwd`):"}}} +{"t_ms": 17356, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "** `/tmp/agy"}}} +{"t_ms": 17382, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "-p2/owned`\n- **Read file"}}} +{"t_ms": 17407, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": ":** [input.txt](file:///w"}}} +{"t_ms": 17433, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "orkspace/inp"}}} +{"t_ms": 17459, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ut.txt) (Content: `PHASE"}}} +{"t_ms": 17484, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "2_WORKSPACE_"}}} +{"t_ms": 17509, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "MARK"}}} +{"t_ms": 17535, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ER`)\n- **Written file:**"}}} +{"t_ms": 17560, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " [output.txt"}}} +{"t_ms": 17586, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "](file:///wor"}}} +{"t_ms": 17611, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "kspace/outpu"}}} +{"t_ms": 17637, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "t"}}} +{"t_ms": 17738, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": ".txt) (and m"}}} +{"t_ms": 17764, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "irrored to [output.txt]("}}} +{"t_ms": 17789, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "file:///tmp/"}}} +{"t_ms": 17814, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "agy-p2/owned/output.txt)"}}} +{"t_ms": 17840, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": ") containing "}}} +{"t_ms": 17941, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "`PHASE2_WRITE`\n\n### Exac"}}} +{"t_ms": 17966, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "t Paths Used\n- Default C"}}} +{"t_ms": 17992, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ommand Worki"}}} +{"t_ms": 18017, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ng Directory: `/tmp/agy-"}}} +{"t_ms": 18043, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "p2/owned"}}} +{"t_ms": 18069, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "`\n- Workspac"}}} +{"t_ms": 18095, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "e Read Path: `/tmp/agy-p"}}} +{"t_ms": 18120, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "2/workspace/"}}} +{"t_ms": 18145, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "input.txt`\n- Output Writ"}}} +{"t_ms": 18171, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ten"}}} +{"t_ms": 18196, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " Paths:\n - `"}}} +{"t_ms": 18222, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "/workspace/o"}}} +{"t_ms": 18247, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "utput.txt`\n - `/tmp"}}} +{"t_ms": 18273, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "/agy-p2/owned/output.txt`"}}} +{"t_ms": 18450, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 3.1541972400000002, "usage": {"input_tokens": 13354, "output_tokens": 262, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 13616}}}} +{"t_ms": 18450, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "status": "SUCCESS", "response": "### MCP Tool Execution\n- Calling the `phase2` MCP `echo` tool failed with an argument validation error from the server schema interface:\n `MCP_UNAVAILABLE` (error: `Invalid input: expected record, received string`)\n\n---\n\n### Shell `pwd` & Workspace File Operations\n- **Current Working Directory (`pwd`):** `/owned`\n- **Read file:** [input.txt](file:///input.txt) (Content: `PHASE2_WORKSPACE_MARKER`)\n- **Written file:** [output.txt](file:///output.txt) (and mirrored to [output.txt](file:///owned/output.txt)) containing `PHASE2_WRITE`\n\n### Exact Paths Used\n- Default Command Working Directory: `/owned`\n- Workspace Read Path: `/input.txt`\n- Output Written Paths:\n - `/output.txt`\n - `/owned/output.txt`\n", "duration_seconds": 18.127914828, "num_turns": 1, "usage": {"input_tokens": 101755, "output_tokens": 1101, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 102856}}}} diff --git a/plugins/antigravity-provider/test/fixtures/model-switch-invalid.capture.ndjson b/plugins/antigravity-provider/test/fixtures/model-switch-invalid.capture.ndjson new file mode 100644 index 0000000000..d9a08ce530 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/model-switch-invalid.capture.ndjson @@ -0,0 +1,3 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", "", "--conversation", "b966b16a-caf4-448a-bb24-0e34956022b0", "--model", "gemini-3.1-flash-lite-preview"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 4004, "stream": "capture", "data": {"timeout_waiting_for": "init", "seconds": 4}} +{"t_ms": 4005, "stream": "capture", "data": {"exit_code": 1}} diff --git a/plugins/antigravity-provider/test/fixtures/model-switch-invalid.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/model-switch-invalid.stderr.ndjson new file mode 100644 index 0000000000..244d1ac13e --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/model-switch-invalid.stderr.ndjson @@ -0,0 +1,13 @@ +{"t_ms": 296, "stream": "stderr", "data": "error: invalid model selection (--model \"gemini-3.1-flash-lite-preview\" --effort \"\"): model gemini-3.1-flash-lite-preview is not recognized as a known model or custom model in settings"} +{"t_ms": 296, "stream": "stderr", "data": "Available models:"} +{"t_ms": 296, "stream": "stderr", "data": " Gemini 3.8 Flash (High)"} +{"t_ms": 296, "stream": "stderr", "data": " Gemini 3.8 Flash (Medium)"} +{"t_ms": 296, "stream": "stderr", "data": " Gemini 3.8 Flash (Low)"} +{"t_ms": 296, "stream": "stderr", "data": " Gemini 3.7 Flash (High)"} +{"t_ms": 296, "stream": "stderr", "data": " Gemini 3.7 Flash (Medium)"} +{"t_ms": 296, "stream": "stderr", "data": " Gemini 3.7 Flash (Low)"} +{"t_ms": 296, "stream": "stderr", "data": " Gemini 3.6 Flash (High)"} +{"t_ms": 296, "stream": "stderr", "data": " Gemini 3.6 Flash (Medium)"} +{"t_ms": 296, "stream": "stderr", "data": " Gemini 3.6 Flash (Low)"} +{"t_ms": 297, "stream": "stderr", "data": " Gemini 3.1 Pro (High)"} +{"t_ms": 297, "stream": "stderr", "data": " Gemini 3.1 Pro (Low)"} diff --git a/plugins/antigravity-provider/test/fixtures/model-switch-invalid.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/model-switch-invalid.stdin.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/model-switch-invalid.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/model-switch-invalid.stdout.ndjson new file mode 100644 index 0000000000..1ad10e7494 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/model-switch-invalid.stdout.ndjson @@ -0,0 +1 @@ +{"t_ms": 297, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "", "status": "ERROR", "response": "", "error": "invalid model selection (--model \"gemini-3.1-flash-lite-preview\" --effort \"\"): model gemini-3.1-flash-lite-preview is not recognized as a known model or custom model in settings\nAvailable models:\n Gemini 3.8 Flash (High)\n Gemini 3.8 Flash (Medium)\n Gemini 3.8 Flash (Low)\n Gemini 3.7 Flash (High)\n Gemini 3.7 Flash (Medium)\n Gemini 3.7 Flash (Low)\n Gemini 3.6 Flash (High)\n Gemini 3.6 Flash (Medium)\n Gemini 3.6 Flash (Low)\n Gemini 3.1 Pro (High)\n Gemini 3.1 Pro (Low)", "duration_seconds": 0, "num_turns": 0, "usage": {"input_tokens": 0, "output_tokens": 0, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 0}}}} diff --git a/plugins/antigravity-provider/test/fixtures/model-switch-network.capture.ndjson b/plugins/antigravity-provider/test/fixtures/model-switch-network.capture.ndjson new file mode 100644 index 0000000000..243104c416 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/model-switch-network.capture.ndjson @@ -0,0 +1,3 @@ +{"t_ms": 1, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--conversation", "b966b16a-caf4-448a-bb24-0e34956022b0", "--model", "gemini-3.8-flash-low"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 2405, "stream": "capture", "data": {"translator_http": {"request": 6, "path": "/v1beta/models/gemini-3.8-flash:streamGenerateContent?alt=sse", "model": "google/gemini-3.7-flash", "roles": ["system", "user", "assistant", "user", "assistant", "user", "user", "assistant", "user", "user", "assistant", "user", "user"], "tools": ["view_file", "run_command", "manage_task", "send_message", "schedule", "invoke_subagent", "define_subagent", "manage_subagents", "write_to_file", "replace_file_content", "generate_image", "read_url_content", "search_web", "ask_question"], "usage": {"prompt_tokens": 12105, "completion_tokens": 12, "total_tokens": 12117, "cost": 0.00912375, "is_byok": false, "prompt_tokens_details": {"cached_tokens": 0, "cache_write_tokens": 0, "audio_tokens": 0, "video_tokens": 0}, "cost_details": {"upstream_inference_cost": 0.00912375, "upstream_inference_prompt_cost": 0.00907875, "upstream_inference_completions_cost": 4.5e-05}, "completion_tokens_details": {"reasoning_tokens": 0, "image_tokens": 0, "audio_tokens": 0}}, "toolCalls": [], "status": 200}}} +{"t_ms": 2572, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/model-switch-network.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/model-switch-network.stderr.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/model-switch-network.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/model-switch-network.stdin.ndjson new file mode 100644 index 0000000000..eb33e7913d --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/model-switch-network.stdin.ndjson @@ -0,0 +1 @@ +{"t_ms": 439, "stream": "stdin", "data": {"event": "user", "message": {"content": "What secret word did I give you? No tools."}}} diff --git a/plugins/antigravity-provider/test/fixtures/model-switch-network.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/model-switch-network.stdout.ndjson new file mode 100644 index 0000000000..e1f755f3f2 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/model-switch-network.stdout.ndjson @@ -0,0 +1,8 @@ +{"t_ms": 439, "stream": "stdout", "data": {"event": "init", "conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "init": {"model": "gemini-3.8-flash-low", "cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "request-review"}}} +{"t_ms": 560, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 10, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 560, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 11, "state": "DONE", "step_type": "system_message", "duration_seconds": 9.9025e-05}}} +{"t_ms": 2045, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 12, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "The secret"}}} +{"t_ms": 2070, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 12, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " word you ga"}}} +{"t_ms": 2096, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 12, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ve me is **COBALT**."}}} +{"t_ms": 2405, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 12, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 1.873175086, "usage": {"input_tokens": 12105, "output_tokens": 12, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12117}}}} +{"t_ms": 2405, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "status": "SUCCESS", "response": "The secret word you gave me is **COBALT**.\n", "duration_seconds": 282.68791325, "num_turns": 5, "usage": {"input_tokens": 58633, "output_tokens": 505, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 59138}}}} diff --git a/plugins/antigravity-provider/test/fixtures/model-switch-resume.capture.ndjson b/plugins/antigravity-provider/test/fixtures/model-switch-resume.capture.ndjson new file mode 100644 index 0000000000..961223783a --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/model-switch-resume.capture.ndjson @@ -0,0 +1,3 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", "", "--conversation", "b966b16a-caf4-448a-bb24-0e34956022b0", "--model", "gemini-3.1-flash-high"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 4004, "stream": "capture", "data": {"timeout_waiting_for": "init", "seconds": 4}} +{"t_ms": 4004, "stream": "capture", "data": {"exit_code": 1}} diff --git a/plugins/antigravity-provider/test/fixtures/model-switch-resume.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/model-switch-resume.stderr.ndjson new file mode 100644 index 0000000000..2666337e77 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/model-switch-resume.stderr.ndjson @@ -0,0 +1,13 @@ +{"t_ms": 239, "stream": "stderr", "data": "error: invalid model selection (--model \"gemini-3.1-flash-high\" --effort \"\"): model gemini-3.1-flash-high is not recognized as a known model or custom model in settings"} +{"t_ms": 239, "stream": "stderr", "data": "Available models:"} +{"t_ms": 239, "stream": "stderr", "data": " Gemini 3.8 Flash (High)"} +{"t_ms": 239, "stream": "stderr", "data": " Gemini 3.8 Flash (Medium)"} +{"t_ms": 239, "stream": "stderr", "data": " Gemini 3.8 Flash (Low)"} +{"t_ms": 239, "stream": "stderr", "data": " Gemini 3.7 Flash (High)"} +{"t_ms": 239, "stream": "stderr", "data": " Gemini 3.7 Flash (Medium)"} +{"t_ms": 239, "stream": "stderr", "data": " Gemini 3.7 Flash (Low)"} +{"t_ms": 239, "stream": "stderr", "data": " Gemini 3.6 Flash (High)"} +{"t_ms": 239, "stream": "stderr", "data": " Gemini 3.6 Flash (Medium)"} +{"t_ms": 239, "stream": "stderr", "data": " Gemini 3.6 Flash (Low)"} +{"t_ms": 239, "stream": "stderr", "data": " Gemini 3.1 Pro (High)"} +{"t_ms": 239, "stream": "stderr", "data": " Gemini 3.1 Pro (Low)"} diff --git a/plugins/antigravity-provider/test/fixtures/model-switch-resume.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/model-switch-resume.stdin.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/model-switch-resume.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/model-switch-resume.stdout.ndjson new file mode 100644 index 0000000000..3556e4c951 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/model-switch-resume.stdout.ndjson @@ -0,0 +1 @@ +{"t_ms": 239, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "", "status": "ERROR", "response": "", "error": "invalid model selection (--model \"gemini-3.1-flash-high\" --effort \"\"): model gemini-3.1-flash-high is not recognized as a known model or custom model in settings\nAvailable models:\n Gemini 3.8 Flash (High)\n Gemini 3.8 Flash (Medium)\n Gemini 3.8 Flash (Low)\n Gemini 3.7 Flash (High)\n Gemini 3.7 Flash (Medium)\n Gemini 3.7 Flash (Low)\n Gemini 3.6 Flash (High)\n Gemini 3.6 Flash (Medium)\n Gemini 3.6 Flash (Low)\n Gemini 3.1 Pro (High)\n Gemini 3.1 Pro (Low)", "duration_seconds": 0, "num_turns": 0, "usage": {"input_tokens": 0, "output_tokens": 0, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 0}}}} diff --git a/plugins/antigravity-provider/test/fixtures/model-switch-valid.capture.ndjson b/plugins/antigravity-provider/test/fixtures/model-switch-valid.capture.ndjson new file mode 100644 index 0000000000..9d44fc439e --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/model-switch-valid.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--conversation", "b966b16a-caf4-448a-bb24-0e34956022b0", "--model", "gemini-3.8-flash-low"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 4608, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/model-switch-valid.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/model-switch-valid.stderr.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/model-switch-valid.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/model-switch-valid.stdin.ndjson new file mode 100644 index 0000000000..f1fb9508ac --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/model-switch-valid.stdin.ndjson @@ -0,0 +1 @@ +{"t_ms": 296, "stream": "stdin", "data": {"event": "user", "message": {"content": "What secret word did I give you? No tools."}}} diff --git a/plugins/antigravity-provider/test/fixtures/model-switch-valid.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/model-switch-valid.stdout.ndjson new file mode 100644 index 0000000000..70dbf755eb --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/model-switch-valid.stdout.ndjson @@ -0,0 +1,7 @@ +{"t_ms": 296, "stream": "stdout", "data": {"event": "init", "conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "init": {"model": "gemini-3.8-flash-low", "cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "request-review"}}} +{"t_ms": 342, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 7, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 342, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 8, "state": "DONE", "step_type": "system_message", "duration_seconds": 4.783e-05}}} +{"t_ms": 3891, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "The secret"}}} +{"t_ms": 4018, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " word you gave me is **COBALT**."}}} +{"t_ms": 4473, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 9, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 4.143724393, "usage": {"input_tokens": 11846, "output_tokens": 12, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11858}}}} +{"t_ms": 4499, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "status": "SUCCESS", "response": "The secret word you gave me is **COBALT**.\n", "duration_seconds": 129.476794036, "num_turns": 4, "usage": {"input_tokens": 46528, "output_tokens": 493, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 47021}}}} diff --git a/plugins/antigravity-provider/test/fixtures/models-api-key.capture.ndjson b/plugins/antigravity-provider/test/fixtures/models-api-key.capture.ndjson new file mode 100644 index 0000000000..2db201ce59 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/models-api-key.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "models"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 360, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/models-api-key.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/models-api-key.stderr.ndjson new file mode 100644 index 0000000000..31ed588249 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/models-api-key.stderr.ndjson @@ -0,0 +1 @@ +{"t_ms": 247, "stream": "stderr", "data": "Fetching available models..."} diff --git a/plugins/antigravity-provider/test/fixtures/models-api-key.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/models-api-key.stdin.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/models-api-key.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/models-api-key.stdout.ndjson new file mode 100644 index 0000000000..ece429fc4b --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/models-api-key.stdout.ndjson @@ -0,0 +1,11 @@ +{"t_ms": 248, "stream": "stdout", "data": "gemini-3.8-flash-high\tGemini 3.8 Flash (High)"} +{"t_ms": 248, "stream": "stdout", "data": "gemini-3.8-flash-medium\tGemini 3.8 Flash (Medium)"} +{"t_ms": 248, "stream": "stdout", "data": "gemini-3.8-flash-low\tGemini 3.8 Flash (Low)"} +{"t_ms": 248, "stream": "stdout", "data": "gemini-3.7-flash-high\tGemini 3.7 Flash (High)"} +{"t_ms": 248, "stream": "stdout", "data": "gemini-3.7-flash-medium\tGemini 3.7 Flash (Medium)"} +{"t_ms": 248, "stream": "stdout", "data": "gemini-3.7-flash-low\tGemini 3.7 Flash (Low)"} +{"t_ms": 248, "stream": "stdout", "data": "gemini-3.6-flash-high\tGemini 3.6 Flash (High)"} +{"t_ms": 248, "stream": "stdout", "data": "gemini-3.6-flash-medium\tGemini 3.6 Flash (Medium)"} +{"t_ms": 248, "stream": "stdout", "data": "gemini-3.6-flash-low\tGemini 3.6 Flash (Low)"} +{"t_ms": 248, "stream": "stdout", "data": "gemini-3.1-pro-high\tGemini 3.1 Pro (High)"} +{"t_ms": 248, "stream": "stdout", "data": "gemini-3.1-pro-low\tGemini 3.1 Pro (Low)"} diff --git a/plugins/antigravity-provider/test/fixtures/models-unauthenticated.capture.ndjson b/plugins/antigravity-provider/test/fixtures/models-unauthenticated.capture.ndjson new file mode 100644 index 0000000000..b91f7dfb82 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/models-unauthenticated.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "models"], "cwd": "", "home": "", "settings": null, "env": {}}} +{"t_ms": 567, "stream": "capture", "data": {"exit_code": 1}} diff --git a/plugins/antigravity-provider/test/fixtures/models-unauthenticated.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/models-unauthenticated.stderr.ndjson new file mode 100644 index 0000000000..0bcbc4745c --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/models-unauthenticated.stderr.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 404, "stream": "stderr", "data": "Fetching available models..."} +{"t_ms": 404, "stream": "stderr", "data": "Error: Please sign in to view available models. Launch the CLI without arguments to sign in."} diff --git a/plugins/antigravity-provider/test/fixtures/models-unauthenticated.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/models-unauthenticated.stdin.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/models-unauthenticated.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/models-unauthenticated.stdout.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/multi-turn-reasoning.capture.ndjson b/plugins/antigravity-provider/test/fixtures/multi-turn-reasoning.capture.ndjson new file mode 100644 index 0000000000..56e04c0abe --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/multi-turn-reasoning.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", "", "--disable-slash-commands"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 6732, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/multi-turn-reasoning.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/multi-turn-reasoning.stderr.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/multi-turn-reasoning.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/multi-turn-reasoning.stdin.ndjson new file mode 100644 index 0000000000..4210a3f1b6 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/multi-turn-reasoning.stdin.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 304, "stream": "stdin", "data": {"event": "user", "message": {"role": "user", "content": "Remember the secret word COBALT. Solve 17*23 with reasoning, then reply with the answer. No tools."}}} +{"t_ms": 4752, "stream": "stdin", "data": {"event": "user", "message": {"role": "user", "content": "What secret word did I give you? Reply with only that word. No tools."}}} diff --git a/plugins/antigravity-provider/test/fixtures/multi-turn-reasoning.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/multi-turn-reasoning.stdout.ndjson new file mode 100644 index 0000000000..dd2a346d75 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/multi-turn-reasoning.stdout.ndjson @@ -0,0 +1,51 @@ +{"t_ms": 304, "stream": "stdout", "data": {"event": "init", "conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "request-review"}}} +{"t_ms": 332, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 3128, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "I have noted"}}} +{"t_ms": 3153, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " and remembe"}}} +{"t_ms": 3178, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "red the secret word **CO"}}} +{"t_ms": 3203, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "BALT**.\n\n###"}}} +{"t_ms": 3229, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " Reasoning"}}} +{"t_ms": 3280, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " for $17 \\ti"}}} +{"t_ms": 3306, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "mes 23$:\n\nWe can solve $"}}} +{"t_ms": 3332, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "17 \\times 23$ using the "}}} +{"t_ms": 3357, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "difference o"}}} +{"t_ms": 3382, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "f"}}} +{"t_ms": 3459, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " squares ide"}}} +{"t_ms": 3484, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ntity, $(a - b)(a + b) ="}}} +{"t_ms": 3510, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " a^2 - b^2$:\n\n1. Rewrite"}}} +{"t_ms": 3535, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " the"}}} +{"t_ms": 3688, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " terms centered around 20:\n $$17 ="}}} +{"t_ms": 3713, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " 20 - 3$$\n "}}} +{"t_ms": 3738, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " $$23"}}} +{"t_ms": 3814, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " = 20 + 3$$\n\n2. Apply th"}}} +{"t_ms": 3840, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "e difference"}}} +{"t_ms": 3865, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " of squares formula:\n "}}} +{"t_ms": 3891, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "$$17 \\times "}}} +{"t_ms": 3917, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "23 = (20 - 3)(20 + 3) = 2"}}} +{"t_ms": 3942, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "0^2 - 3"}}} +{"t_ms": 4069, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "^2$$\n\n3. Cal"}}} +{"t_ms": 4094, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "culate the squares:\n $"}}} +{"t_ms": 4119, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "$20^2 = 400$"}}} +{"t_ms": 4145, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "$\n "}}} +{"t_ms": 4170, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "$$3^2 = 9$$\n"}}} +{"t_ms": 4221, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "\n4. Subtract"}}} +{"t_ms": 4246, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": ":\n $$400 -"}}} +{"t_ms": 4271, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " 9 = 3"}}} +{"t_ms": 4323, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "91$$\n\n*(Alte"}}} +{"t_ms": 4348, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "rnatively, v"}}} +{"t_ms": 4398, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ia partial p"}}} +{"t_ms": 4424, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "roducts: $17"}}} +{"t_ms": 4474, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " \\times 20 ="}}} +{"t_ms": 4499, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " 340$, and $"}}} +{"t_ms": 4550, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "17 \\times 3 "}}} +{"t_ms": 4575, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "= 51$; then "}}} +{"t_ms": 4600, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "$340 + 51 = "}}} +{"t_ms": 4650, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "391$.)*\n\n---"}}} +{"t_ms": 4675, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "\n\n**Answer:*"}}} +{"t_ms": 4726, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "* **391**"}}} +{"t_ms": 4752, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 4.392588837, "usage": {"input_tokens": 11263, "output_tokens": 466, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11729}}}} +{"t_ms": 4752, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "status": "SUCCESS", "response": "I have noted and remembered the secret word **COBALT**.\n\n### Reasoning for $17 \\times 23$:\n\nWe can solve $17 \\times 23$ using the difference of squares identity, $(a - b)(a + b) = a^2 - b^2$:\n\n1. Rewrite the terms centered around 20:\n $$17 = 20 - 3$$\n $$23 = 20 + 3$$\n\n2. Apply the difference of squares formula:\n $$17 \\times 23 = (20 - 3)(20 + 3) = 20^2 - 3^2$$\n\n3. Calculate the squares:\n $$20^2 = 400$$\n $$3^2 = 9$$\n\n4. Subtract:\n $$400 - 9 = 391$$\n\n*(Alternatively, via partial products: $17 \\times 20 = 340$, and $17 \\times 3 = 51$; then $340 + 51 = 391$.)*\n\n---\n\n**Answer:** **391**\n", "duration_seconds": 4.414417492, "num_turns": 1, "usage": {"input_tokens": 11263, "output_tokens": 466, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11729}}}} +{"t_ms": 4785, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 2, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 6345, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "COBALT"}}} +{"t_ms": 6573, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 3, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 1.784280065, "usage": {"input_tokens": 11604, "output_tokens": 3, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11607}}}} +{"t_ms": 6573, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "status": "SUCCESS", "response": "COBALT\n", "duration_seconds": 6.242840924, "num_turns": 2, "usage": {"input_tokens": 22867, "output_tokens": 469, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 23336}}}} diff --git a/plugins/antigravity-provider/test/fixtures/not-signed-in.capture.ndjson b/plugins/antigravity-provider/test/fixtures/not-signed-in.capture.ndjson new file mode 100644 index 0000000000..a77dc1e4ee --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/not-signed-in.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json"], "cwd": "", "home": "", "settings": null, "env": {}}} +{"t_ms": 520, "stream": "capture", "data": {"exit_code": 1}} diff --git a/plugins/antigravity-provider/test/fixtures/not-signed-in.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/not-signed-in.stderr.ndjson new file mode 100644 index 0000000000..9304e8b2ee --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/not-signed-in.stderr.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 360, "stream": "stderr", "data": "Error: authentication required. Run 'agy' to log in, then retry."} +{"t_ms": 360, "stream": "stderr", "data": "error: authentication failed or timed out"} diff --git a/plugins/antigravity-provider/test/fixtures/not-signed-in.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/not-signed-in.stdin.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/not-signed-in.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/not-signed-in.stdout.ndjson new file mode 100644 index 0000000000..760f2dbe68 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/not-signed-in.stdout.ndjson @@ -0,0 +1 @@ +{"t_ms": 360, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "", "status": "ERROR", "response": "", "error": "authentication failed or timed out", "duration_seconds": 0, "num_turns": 0, "usage": {"input_tokens": 0, "output_tokens": 0, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 0}}}} diff --git a/plugins/antigravity-provider/test/fixtures/parity-inspection.ndjson b/plugins/antigravity-provider/test/fixtures/parity-inspection.ndjson new file mode 100644 index 0000000000..18c16ef484 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/parity-inspection.ndjson @@ -0,0 +1,10 @@ +{"t_ms": 0, "stream": "inspection", "data": {"type": "configuration", "file": "/owned/.agents/mcp_config.json", "content": {"mcpServers": {"phase2": {"serverUrl": "http://127.0.0.1:/mcp"}}}}} +{"t_ms": 0, "stream": "inspection", "data": {"type": "isolation", "global_mcp_config_exists": {"mcp-second-add-dir": true, "mcp-owned-cwd": true, "mcp-owned-first-add-dir": true, "mcp-agent-file": true, "mcp-agent-definition": true, "mcp-agent-discovered": true, "image-file-external": true}}} +{"t_ms": 0, "stream": "inspection", "data": {"type": "agent_definition", "file": "/owned/agent.md", "content": "---\nname: phase2\ndescription: Session adapter\nmcpServers: {\"phase2\": {\"serverUrl\": \"http://127.0.0.1:/mcp\"}}\n---\nWork in the current workspace.\n"}} +{"t_ms": 0, "stream": "inspection", "data": {"type": "agent_definition", "file": "/owned/.agents/agents/phase2.md", "content": "---\nname: phase2\ndescription: Session adapter\nmainAgent: true\nworkdir: \nmcpServers: {\"phase2\": {\"serverUrl\": \"http://127.0.0.1:/mcp\"}}\n---\n# Identity\nWork in the current workspace.\n"}} +{"t_ms": 0, "stream": "inspection", "data": {"type": "agent_manifest", "file": "/owned/.agents/agents.json", "content": {"entries": [{"path": "/owned/.agents/agents"}]}}} +{"t_ms": 0, "stream": "inspection", "data": {"type": "native_log", "capture": "mcp-agent-file", "line": "W0929 21:29:08.043649 1 session.go:94] Agent \"/owned/agent.md\" not found, falling back to default"}} +{"t_ms": 0, "stream": "inspection", "data": {"type": "native_log", "capture": "mcp-agent-definition", "line": "W0929 21:30:59.337788 1 session.go:94] Agent \"phase2\" not found, falling back to default"}} +{"t_ms": 0, "stream": "inspection", "data": {"type": "native_log", "capture": "mcp-agent-discovered", "line": "W0929 21:33:13.643983 1 session.go:94] Agent \"phase2\" not found, falling back to default"}} +{"t_ms": 0, "stream": "inspection", "data": {"type": "translator_image_part", "function": "view_file", "location": "functionResponse.parts[].inlineData", "mimeType": "image/png", "forwarded": true}} +{"t_ms": 0, "stream": "inspection", "data": {"type": "image", "fixture": "image-file-external.png", "sha256": "abd9ec3ff865aa30218d6184eadff4c0fc1c55d6ada251909d6b4554d64d5336", "expected": "Red triangle on the left, blue circle on the lower right, white background."}} diff --git a/plugins/antigravity-provider/test/fixtures/project-skill.capture.ndjson b/plugins/antigravity-provider/test/fixtures/project-skill.capture.ndjson new file mode 100644 index 0000000000..c7e091ebe3 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/project-skill.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", "", "--disable-slash-commands"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 5673, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/project-skill.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/project-skill.stderr.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/project-skill.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/project-skill.stdin.ndjson new file mode 100644 index 0000000000..2380ec6f8e --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/project-skill.stdin.ndjson @@ -0,0 +1 @@ +{"t_ms": 361, "stream": "stdin", "data": {"event": "user", "message": {"content": "Use the phase0-marker skill now."}}} diff --git a/plugins/antigravity-provider/test/fixtures/project-skill.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/project-skill.stdout.ndjson new file mode 100644 index 0000000000..d2a8a0b936 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/project-skill.stdout.ndjson @@ -0,0 +1,9 @@ +{"t_ms": 361, "stream": "stdout", "data": {"event": "init", "conversation_id": "a145f6cf-b974-4ddb-b7eb-58bca63a0092", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "request-review"}}} +{"t_ms": 415, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a145f6cf-b974-4ddb-b7eb-58bca63a0092", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 3806, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a145f6cf-b974-4ddb-b7eb-58bca63a0092", "step_index": 1, "state": "DONE", "step_type": "agent_response", "duration_seconds": 3.389284511, "usage": {"input_tokens": 11270, "output_tokens": 246, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11516}}}} +{"t_ms": 3806, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a145f6cf-b974-4ddb-b7eb-58bca63a0092", "step_index": 2, "state": "ACTIVE", "step_type": "tool", "tool_name": "view_file", "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/.agents/skills/phase0-marker/SKILL.md"}}}}} +{"t_ms": 3806, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a145f6cf-b974-4ddb-b7eb-58bca63a0092", "step_index": 2, "state": "DONE", "step_type": "tool", "tool_name": "view_file", "duration_seconds": 0.003649634, "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/.agents/skills/phase0-marker/SKILL.md"}, "output": "6 lines, 163 bytes"}}}} +{"t_ms": 5190, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a145f6cf-b974-4ddb-b7eb-58bca63a0092", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "PHASE"}}} +{"t_ms": 5216, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a145f6cf-b974-4ddb-b7eb-58bca63a0092", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "0_SKILL_MARKER"}}} +{"t_ms": 5572, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a145f6cf-b974-4ddb-b7eb-58bca63a0092", "step_index": 3, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 1.7500252870000002, "usage": {"input_tokens": 11562, "output_tokens": 9, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11571}}}} +{"t_ms": 5572, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "a145f6cf-b974-4ddb-b7eb-58bca63a0092", "status": "SUCCESS", "response": "PHASE0_SKILL_MARKER\n", "duration_seconds": 5.185824959, "num_turns": 1, "usage": {"input_tokens": 22832, "output_tokens": 255, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 23087}}}} diff --git a/plugins/antigravity-provider/test/fixtures/question-auto-denial.capture.ndjson b/plugins/antigravity-provider/test/fixtures/question-auto-denial.capture.ndjson new file mode 100644 index 0000000000..f2e339bb07 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/question-auto-denial.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", ""], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 7149, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/question-auto-denial.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/question-auto-denial.stderr.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/question-auto-denial.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/question-auto-denial.stdin.ndjson new file mode 100644 index 0000000000..b53fbe5176 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/question-auto-denial.stdin.ndjson @@ -0,0 +1 @@ +{"t_ms": 298, "stream": "stdin", "data": {"event": "user", "message": {"content": "Use ask_question to ask me which color I prefer, red or blue. Do not choose for me."}}} diff --git a/plugins/antigravity-provider/test/fixtures/question-auto-denial.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/question-auto-denial.stdout.ndjson new file mode 100644 index 0000000000..352ea75867 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/question-auto-denial.stdout.ndjson @@ -0,0 +1,14 @@ +{"t_ms": 298, "stream": "stdout", "data": {"event": "init", "conversation_id": "e81b4d91-ca0f-4bf9-9841-d44ec1f48997", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "request-review"}}} +{"t_ms": 325, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e81b4d91-ca0f-4bf9-9841-d44ec1f48997", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 4334, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e81b4d91-ca0f-4bf9-9841-d44ec1f48997", "step_index": 1, "state": "DONE", "step_type": "agent_response", "duration_seconds": 3.994201944, "usage": {"input_tokens": 11258, "output_tokens": 297, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11555}}}} +{"t_ms": 4334, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e81b4d91-ca0f-4bf9-9841-d44ec1f48997", "step_index": 2, "state": "DONE", "step_type": "unknown", "duration_seconds": 0.00288314}}} +{"t_ms": 6347, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e81b4d91-ca0f-4bf9-9841-d44ec1f48997", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "You"}}} +{"t_ms": 6676, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e81b4d91-ca0f-4bf9-9841-d44ec1f48997", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " skipped ans"}}} +{"t_ms": 6702, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e81b4d91-ca0f-4bf9-9841-d44ec1f48997", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "wering the question. Let"}}} +{"t_ms": 6728, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e81b4d91-ca0f-4bf9-9841-d44ec1f48997", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " me know if you'd like t"}}} +{"t_ms": 6753, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e81b4d91-ca0f-4bf9-9841-d44ec1f48997", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "o choose a c"}}} +{"t_ms": 6778, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e81b4d91-ca0f-4bf9-9841-d44ec1f48997", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "olor or if there's anyth"}}} +{"t_ms": 6804, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e81b4d91-ca0f-4bf9-9841-d44ec1f48997", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ing else I can help you "}}} +{"t_ms": 6829, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e81b4d91-ca0f-4bf9-9841-d44ec1f48997", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "with!"}}} +{"t_ms": 7057, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e81b4d91-ca0f-4bf9-9841-d44ec1f48997", "step_index": 3, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 2.727510169, "usage": {"input_tokens": 11366, "output_tokens": 31, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11397}}}} +{"t_ms": 7057, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "e81b4d91-ca0f-4bf9-9841-d44ec1f48997", "status": "SUCCESS", "response": "You skipped answering the question. Let me know if you'd like to choose a color or if there's anything else I can help you with!\n", "duration_seconds": 6.748385314, "num_turns": 1, "usage": {"input_tokens": 22624, "output_tokens": 328, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 22952}}}} diff --git a/plugins/antigravity-provider/test/fixtures/resume-after-interrupt.capture.ndjson b/plugins/antigravity-provider/test/fixtures/resume-after-interrupt.capture.ndjson new file mode 100644 index 0000000000..381dc3897e --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/resume-after-interrupt.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--conversation", "a2c2132f-0f10-400b-a142-74f82230f449"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 2531, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/resume-after-interrupt.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/resume-after-interrupt.stderr.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/resume-after-interrupt.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/resume-after-interrupt.stdin.ndjson new file mode 100644 index 0000000000..40ea297e7f --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/resume-after-interrupt.stdin.ndjson @@ -0,0 +1 @@ +{"t_ms": 357, "stream": "stdin", "data": {"event": "user", "message": {"content": "Reply with PHASE0_RECOVERED. No tools."}}} diff --git a/plugins/antigravity-provider/test/fixtures/resume-after-interrupt.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/resume-after-interrupt.stdout.ndjson new file mode 100644 index 0000000000..8b3e527418 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/resume-after-interrupt.stdout.ndjson @@ -0,0 +1,7 @@ +{"t_ms": 357, "stream": "stdout", "data": {"event": "init", "conversation_id": "a2c2132f-0f10-400b-a142-74f82230f449", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "request-review"}}} +{"t_ms": 386, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a2c2132f-0f10-400b-a142-74f82230f449", "step_index": 2, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 412, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a2c2132f-0f10-400b-a142-74f82230f449", "step_index": 3, "state": "DONE", "step_type": "system_message", "duration_seconds": 6.4942e-05}}} +{"t_ms": 2228, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a2c2132f-0f10-400b-a142-74f82230f449", "step_index": 4, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "PHASE0_RECOV"}}} +{"t_ms": 2253, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a2c2132f-0f10-400b-a142-74f82230f449", "step_index": 4, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ERED"}}} +{"t_ms": 2380, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a2c2132f-0f10-400b-a142-74f82230f449", "step_index": 4, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 1.9710905699999999, "usage": {"input_tokens": 11524, "output_tokens": 65, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11589}}}} +{"t_ms": 2380, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "a2c2132f-0f10-400b-a142-74f82230f449", "status": "SUCCESS", "response": "PHASE0_RECOVERED\n", "duration_seconds": 216.301687963, "num_turns": 2, "usage": {"input_tokens": 11524, "output_tokens": 65, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11589}}}} diff --git a/plugins/antigravity-provider/test/fixtures/resume.capture.ndjson b/plugins/antigravity-provider/test/fixtures/resume.capture.ndjson new file mode 100644 index 0000000000..4caec009da --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/resume.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", "", "--disable-slash-commands", "--conversation", "b966b16a-caf4-448a-bb24-0e34956022b0"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 2317, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/resume.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/resume.stderr.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/resume.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/resume.stdin.ndjson new file mode 100644 index 0000000000..e91cc571bd --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/resume.stdin.ndjson @@ -0,0 +1 @@ +{"t_ms": 288, "stream": "stdin", "data": {"event": "user", "message": {"role": "user", "content": "What secret word did I give you? No tools."}}} diff --git a/plugins/antigravity-provider/test/fixtures/resume.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/resume.stdout.ndjson new file mode 100644 index 0000000000..d685e83cfb --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/resume.stdout.ndjson @@ -0,0 +1,8 @@ +{"t_ms": 288, "stream": "stdout", "data": {"event": "init", "conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "request-review"}}} +{"t_ms": 333, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 4, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 333, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 5, "state": "DONE", "step_type": "system_message", "duration_seconds": 4.3562e-05}}} +{"t_ms": 1969, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 6, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "The secret"}}} +{"t_ms": 2045, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 6, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " word you gave me is **C"}}} +{"t_ms": 2070, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 6, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "OBALT**."}}} +{"t_ms": 2172, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 6, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 1.821180269, "usage": {"input_tokens": 11815, "output_tokens": 12, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11827}}}} +{"t_ms": 2172, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "status": "SUCCESS", "response": "The secret word you gave me is **COBALT**.\n", "duration_seconds": 8.574573367, "num_turns": 3, "usage": {"input_tokens": 34682, "output_tokens": 481, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 35163}}}} diff --git a/plugins/antigravity-provider/test/fixtures/schema-prompt.capture.ndjson b/plugins/antigravity-provider/test/fixtures/schema-prompt.capture.ndjson new file mode 100644 index 0000000000..9964112608 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/schema-prompt.capture.ndjson @@ -0,0 +1,3 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 30706, "stream": "capture", "data": {"timeout_waiting_for": "result", "seconds": 30}} +{"t_ms": 30794, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/schema-prompt.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/schema-prompt.stderr.ndjson new file mode 100644 index 0000000000..0a830c1e99 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/schema-prompt.stderr.ndjson @@ -0,0 +1 @@ +{"t_ms": 677, "stream": "stderr", "data": "warning: ignoring unsupported stream input message event \"prompt\""} diff --git a/plugins/antigravity-provider/test/fixtures/schema-prompt.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/schema-prompt.stdin.ndjson new file mode 100644 index 0000000000..2831eab45f --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/schema-prompt.stdin.ndjson @@ -0,0 +1 @@ +{"t_ms": 677, "stream": "stdin", "data": {"event": "prompt", "prompt": "Reply PHASE0_HELLO."}} diff --git a/plugins/antigravity-provider/test/fixtures/schema-prompt.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/schema-prompt.stdout.ndjson new file mode 100644 index 0000000000..1b83a4d41e --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/schema-prompt.stdout.ndjson @@ -0,0 +1 @@ +{"t_ms": 676, "stream": "stdout", "data": {"event": "init", "conversation_id": "11a830e0-80b1-4f55-ab34-0670be20a722", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "request-review"}}} diff --git a/plugins/antigravity-provider/test/fixtures/schema-user-message.capture.ndjson b/plugins/antigravity-provider/test/fixtures/schema-user-message.capture.ndjson new file mode 100644 index 0000000000..8db1f98058 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/schema-user-message.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 490, "stream": "capture", "data": {"exit_code": 1}} diff --git a/plugins/antigravity-provider/test/fixtures/schema-user-message.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/schema-user-message.stderr.ndjson new file mode 100644 index 0000000000..30d0fa5adb --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/schema-user-message.stderr.ndjson @@ -0,0 +1 @@ +{"t_ms": 386, "stream": "stderr", "data": "error: failed to decode stream input: json: cannot unmarshal string into Go struct field streamInputMessage.message of type printmode.streamInputUserMessage"} diff --git a/plugins/antigravity-provider/test/fixtures/schema-user-message.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/schema-user-message.stdin.ndjson new file mode 100644 index 0000000000..cbedcbaceb --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/schema-user-message.stdin.ndjson @@ -0,0 +1 @@ +{"t_ms": 383, "stream": "stdin", "data": {"event": "user_message", "message": "Reply PHASE0_HELLO."}} diff --git a/plugins/antigravity-provider/test/fixtures/schema-user-message.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/schema-user-message.stdout.ndjson new file mode 100644 index 0000000000..333d3a9b8d --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/schema-user-message.stdout.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 383, "stream": "stdout", "data": {"event": "init", "conversation_id": "b70d4858-e4ed-469c-84d6-8fec25ceb1cb", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "request-review"}}} +{"t_ms": 386, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "b70d4858-e4ed-469c-84d6-8fec25ceb1cb", "status": "ERROR", "response": "", "error": "failed to decode stream input: json: cannot unmarshal string into Go struct field streamInputMessage.message of type printmode.streamInputUserMessage", "duration_seconds": 0, "num_turns": 0, "usage": {"input_tokens": 0, "output_tokens": 0, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 0}}}} diff --git a/plugins/antigravity-provider/test/fixtures/schema-user.capture.ndjson b/plugins/antigravity-provider/test/fixtures/schema-user.capture.ndjson new file mode 100644 index 0000000000..205f7c638b --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/schema-user.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 426, "stream": "capture", "data": {"exit_code": 1}} diff --git a/plugins/antigravity-provider/test/fixtures/schema-user.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/schema-user.stderr.ndjson new file mode 100644 index 0000000000..7ae241a87e --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/schema-user.stderr.ndjson @@ -0,0 +1 @@ +{"t_ms": 342, "stream": "stderr", "data": "error: failed to decode stream input: json: cannot unmarshal string into Go struct field streamInputMessage.message of type printmode.streamInputUserMessage"} diff --git a/plugins/antigravity-provider/test/fixtures/schema-user.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/schema-user.stdin.ndjson new file mode 100644 index 0000000000..5523b07666 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/schema-user.stdin.ndjson @@ -0,0 +1 @@ +{"t_ms": 338, "stream": "stdin", "data": {"event": "user", "message": "Reply PHASE0_HELLO."}} diff --git a/plugins/antigravity-provider/test/fixtures/schema-user.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/schema-user.stdout.ndjson new file mode 100644 index 0000000000..324e16471e --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/schema-user.stdout.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 338, "stream": "stdout", "data": {"event": "init", "conversation_id": "f57811d9-3f9d-42cc-870a-ffdaa85ba608", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "request-review"}}} +{"t_ms": 342, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "f57811d9-3f9d-42cc-870a-ffdaa85ba608", "status": "ERROR", "response": "", "error": "failed to decode stream input: json: cannot unmarshal string into Go struct field streamInputMessage.message of type printmode.streamInputUserMessage", "duration_seconds": 0, "num_turns": 0, "usage": {"input_tokens": 0, "output_tokens": 0, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 0}}}} diff --git a/plugins/antigravity-provider/test/fixtures/sigint-mid-text.capture.ndjson b/plugins/antigravity-provider/test/fixtures/sigint-mid-text.capture.ndjson new file mode 100644 index 0000000000..744e6a79b1 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/sigint-mid-text.capture.ndjson @@ -0,0 +1,3 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", "", "--disable-slash-commands", "--dangerously-skip-permissions"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 2000, "stream": "capture", "data": {"signal": "SIGINT", "trigger": {"conversation_id": "a2c2132f-0f10-400b-a142-74f82230f449", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "# The Architecture of In"}}} +{"t_ms": 2141, "stream": "capture", "data": {"exit_code": 1}} diff --git a/plugins/antigravity-provider/test/fixtures/sigint-mid-text.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/sigint-mid-text.stderr.ndjson new file mode 100644 index 0000000000..49b703e524 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/sigint-mid-text.stderr.ndjson @@ -0,0 +1 @@ +{"t_ms": 2025, "stream": "stderr", "data": "error: interrupted"} diff --git a/plugins/antigravity-provider/test/fixtures/sigint-mid-text.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/sigint-mid-text.stdin.ndjson new file mode 100644 index 0000000000..06c4053234 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/sigint-mid-text.stdin.ndjson @@ -0,0 +1 @@ +{"t_ms": 310, "stream": "stdin", "data": {"event": "user", "message": {"content": "Write a long 2000-word essay on prime numbers. No tools."}}} diff --git a/plugins/antigravity-provider/test/fixtures/sigint-mid-text.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/sigint-mid-text.stdout.ndjson new file mode 100644 index 0000000000..63b9bd96f1 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/sigint-mid-text.stdout.ndjson @@ -0,0 +1,4 @@ +{"t_ms": 309, "stream": "stdout", "data": {"event": "init", "conversation_id": "a2c2132f-0f10-400b-a142-74f82230f449", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "always-proceed"}}} +{"t_ms": 337, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a2c2132f-0f10-400b-a142-74f82230f449", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 2000, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a2c2132f-0f10-400b-a142-74f82230f449", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "# The Architecture of In"}}} +{"t_ms": 2025, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "a2c2132f-0f10-400b-a142-74f82230f449", "status": "ERROR", "response": "# The Architecture of In", "error": "interrupted", "duration_seconds": 1.690084523, "num_turns": 1, "usage": {"input_tokens": 0, "output_tokens": 0, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 0}}}} diff --git a/plugins/antigravity-provider/test/fixtures/sigint-mid-tool.capture.ndjson b/plugins/antigravity-provider/test/fixtures/sigint-mid-tool.capture.ndjson new file mode 100644 index 0000000000..d4774ef9e5 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/sigint-mid-tool.capture.ndjson @@ -0,0 +1,3 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", "", "--dangerously-skip-permissions"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 4486, "stream": "capture", "data": {"signal": "SIGINT", "trigger": {"conversation_id": "b3c8b9d6-c0ca-4444-8fd3-8bc86de8e6ba", "step_index": 2, "state": "ACTIVE", "step_type": "tool", "tool_name": "run_command", "tool_info": {"name": "run_command", "parameters": {"CommandLine": "sleep 30; printf FINISHED"}}}}} +{"t_ms": 4716, "stream": "capture", "data": {"exit_code": 1}} diff --git a/plugins/antigravity-provider/test/fixtures/sigint-mid-tool.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/sigint-mid-tool.stderr.ndjson new file mode 100644 index 0000000000..40ea382edb --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/sigint-mid-tool.stderr.ndjson @@ -0,0 +1 @@ +{"t_ms": 4489, "stream": "stderr", "data": "error: interrupted"} diff --git a/plugins/antigravity-provider/test/fixtures/sigint-mid-tool.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/sigint-mid-tool.stdin.ndjson new file mode 100644 index 0000000000..7d34666611 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/sigint-mid-tool.stdin.ndjson @@ -0,0 +1 @@ +{"t_ms": 371, "stream": "stdin", "data": {"event": "user", "message": {"content": "Use run_command to run sleep 30; printf FINISHED in , WaitMsBeforeAsync 1000. Wait until it finishes."}}} diff --git a/plugins/antigravity-provider/test/fixtures/sigint-mid-tool.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/sigint-mid-tool.stdout.ndjson new file mode 100644 index 0000000000..ea8cee886f --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/sigint-mid-tool.stdout.ndjson @@ -0,0 +1,5 @@ +{"t_ms": 371, "stream": "stdout", "data": {"event": "init", "conversation_id": "b3c8b9d6-c0ca-4444-8fd3-8bc86de8e6ba", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "always-proceed"}}} +{"t_ms": 399, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b3c8b9d6-c0ca-4444-8fd3-8bc86de8e6ba", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 4086, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b3c8b9d6-c0ca-4444-8fd3-8bc86de8e6ba", "step_index": 1, "state": "DONE", "step_type": "agent_response", "duration_seconds": 3.660004339, "usage": {"input_tokens": 11272, "output_tokens": 247, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11519}}}} +{"t_ms": 4086, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b3c8b9d6-c0ca-4444-8fd3-8bc86de8e6ba", "step_index": 2, "state": "ACTIVE", "step_type": "tool", "tool_name": "run_command", "tool_info": {"name": "run_command", "parameters": {"CommandLine": "sleep 30; printf FINISHED"}}}}} +{"t_ms": 4489, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "b3c8b9d6-c0ca-4444-8fd3-8bc86de8e6ba", "status": "ERROR", "response": "", "error": "interrupted", "duration_seconds": 4.114884661, "num_turns": 1, "usage": {"input_tokens": 11272, "output_tokens": 247, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11519}}}} diff --git a/plugins/antigravity-provider/test/fixtures/sigterm.capture.ndjson b/plugins/antigravity-provider/test/fixtures/sigterm.capture.ndjson new file mode 100644 index 0000000000..eafb4b76ed --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/sigterm.capture.ndjson @@ -0,0 +1,3 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", "", "--disable-slash-commands", "--dangerously-skip-permissions"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 316, "stream": "capture", "data": {"signal": "SIGTERM", "trigger": {"conversation_id": "21c82404-cd16-4781-a827-eb7186d61b7e", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 467, "stream": "capture", "data": {"exit_code": 1}} diff --git a/plugins/antigravity-provider/test/fixtures/sigterm.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/sigterm.stderr.ndjson new file mode 100644 index 0000000000..ee58454e54 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/sigterm.stderr.ndjson @@ -0,0 +1 @@ +{"t_ms": 341, "stream": "stderr", "data": "error: interrupted"} diff --git a/plugins/antigravity-provider/test/fixtures/sigterm.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/sigterm.stdin.ndjson new file mode 100644 index 0000000000..27e2a28a9c --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/sigterm.stdin.ndjson @@ -0,0 +1 @@ +{"t_ms": 289, "stream": "stdin", "data": {"event": "user", "message": {"content": "Write a long essay on prime numbers. No tools."}}} diff --git a/plugins/antigravity-provider/test/fixtures/sigterm.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/sigterm.stdout.ndjson new file mode 100644 index 0000000000..1fe0f471e1 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/sigterm.stdout.ndjson @@ -0,0 +1,3 @@ +{"t_ms": 289, "stream": "stdout", "data": {"event": "init", "conversation_id": "21c82404-cd16-4781-a827-eb7186d61b7e", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "always-proceed"}}} +{"t_ms": 316, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "21c82404-cd16-4781-a827-eb7186d61b7e", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 341, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "21c82404-cd16-4781-a827-eb7186d61b7e", "status": "ERROR", "response": "", "error": "interrupted", "duration_seconds": 0, "num_turns": 1, "usage": {"input_tokens": 0, "output_tokens": 0, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 0}}}} diff --git a/plugins/antigravity-provider/test/fixtures/slash-disabled.capture.ndjson b/plugins/antigravity-provider/test/fixtures/slash-disabled.capture.ndjson new file mode 100644 index 0000000000..72dc5418fc --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/slash-disabled.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--disable-slash-commands"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 9267, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/slash-disabled.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/slash-disabled.stderr.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/slash-disabled.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/slash-disabled.stdin.ndjson new file mode 100644 index 0000000000..fc08c01367 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/slash-disabled.stdin.ndjson @@ -0,0 +1 @@ +{"t_ms": 292, "stream": "stdin", "data": {"event": "user", "message": {"content": "/help"}}} diff --git a/plugins/antigravity-provider/test/fixtures/slash-disabled.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/slash-disabled.stdout.ndjson new file mode 100644 index 0000000000..5050830bb3 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/slash-disabled.stdout.ndjson @@ -0,0 +1,84 @@ +{"t_ms": 291, "stream": "stdout", "data": {"event": "init", "conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "request-review"}}} +{"t_ms": 318, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 3215, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 1, "state": "DONE", "step_type": "agent_response", "duration_seconds": 2.874473262, "usage": {"input_tokens": 11225, "output_tokens": 193, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11418}}}} +{"t_ms": 3215, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 2, "state": "ACTIVE", "step_type": "tool", "tool_name": "view_file", "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/.gemini/antigravity-cli/builtin/skills/antigravity_guide/SKILL.md"}}}}} +{"t_ms": 3215, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 2, "state": "DONE", "step_type": "tool", "tool_name": "view_file", "duration_seconds": 0.003097441, "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/.gemini/antigravity-cli/builtin/skills/antigravity_guide/SKILL.md"}, "output": "56 lines, 3023 bytes"}}}} +{"t_ms": 5737, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 3, "state": "DONE", "step_type": "agent_response", "duration_seconds": 2.513209521, "usage": {"input_tokens": 12487, "output_tokens": 73, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12560}}}} +{"t_ms": 5737, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 4, "state": "ACTIVE", "step_type": "tool", "tool_name": "view_file", "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/.gemini/antigravity-cli/builtin/skills/antigravity_guide/references/cli.md"}}}}} +{"t_ms": 5737, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 4, "state": "DONE", "step_type": "tool", "tool_name": "view_file", "duration_seconds": 0.003075841, "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/.gemini/antigravity-cli/builtin/skills/antigravity_guide/references/cli.md"}, "output": "42 lines, 1686 bytes"}}}} +{"t_ms": 6974, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "Here is"}}} +{"t_ms": 7051, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " a quick guide to using "}}} +{"t_ms": 7076, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "**Antigravit"}}} +{"t_ms": 7101, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "y (AGY)** and its availa"}}} +{"t_ms": 7127, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ble slash"}}} +{"t_ms": 7177, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " commands:\n\n### Available Slash Comm"}}} +{"t_ms": 7203, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ands\n\n* **/p"}}} +{"t_ms": 7228, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "lan** – Initiates deta"}}} +{"t_ms": 7254, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "iled step-by"}}} +{"t_ms": 7279, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "-step planning and archi"}}} +{"t_ms": 7305, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "tecture desi"}}} +{"t_ms": 7330, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "gn"}}} +{"t_ms": 7356, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " before making code changes.\n* **/gr"}}} +{"t_ms": 7381, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ill-me** –"}}} +{"t_ms": 7407, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " Starts an interactive i"}}} +{"t_ms": 7432, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "nterview/rev"}}} +{"t_ms": 7458, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "iew session to refine re"}}} +{"t_ms": 7483, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "quirements and"}}} +{"t_ms": 7535, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " architectural decisions"}}} +{"t_ms": 7560, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": ".\n* **/goal*"}}} +{"t_ms": 7586, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "* – Sets a high-level,"}}} +{"t_ms": 7611, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " long-running goal where"}}} +{"t_ms": 7637, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " the agent w"}}} +{"t_ms": 7662, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "orks autonomously and"}}} +{"t_ms": 7763, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " thoroughly "}}} +{"t_ms": 7789, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "until completion.\n* **/s"}}} +{"t_ms": 7814, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "chedule** "}}} +{"t_ms": 7840, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "– Sets up one-shot tim"}}} +{"t_ms": 7865, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ers or recur"}}} +{"t_ms": 7890, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ring cron jobs for periodic checks a"}}} +{"t_ms": 7916, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "nd background tasks.\n* *"}}} +{"t_ms": 7941, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "*/browser** "}}} +{"t_ms": 7966, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "– Directs browser auto"}}} +{"t_ms": 7992, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "mation, test"}}} +{"t_ms": 8017, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ing, and web research wo"}}} +{"t_ms": 8042, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "rkflows.\n* **/learn** "}}} +{"t_ms": 8068, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "– Teaches the agent pr"}}} +{"t_ms": 8094, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "eferences, project rules"}}} +{"t_ms": 8119, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": ", or recurri"}}} +{"t_ms": 8144, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ng setup procedures to p"}}} +{"t_ms": 8169, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ersist across future ses"}}} +{"t_ms": 8195, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "sions.\n\n---\n"}}} +{"t_ms": 8220, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "\n### What I Can Help You"}}} +{"t_ms": 8246, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " With\n\n*"}}} +{"t_ms": 8271, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " **Code Exploration & Ed"}}} +{"t_ms": 8296, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "iting**: Sea"}}} +{"t_ms": 8322, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "rch codebase, view/edit "}}} +{"t_ms": 8347, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "files, run tests, and de"}}} +{"t_ms": 8372, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "bug issues.\n"}}} +{"t_ms": 8398, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "* **Ref"}}} +{"t_ms": 8500, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "actoring & F"}}} +{"t_ms": 8525, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "eature Development**: Pl"}}} +{"t_ms": 8550, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "an, implemen"}}} +{"t_ms": 8576, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "t, and verify multi-file"}}} +{"t_ms": 8601, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " features.\n*"}}} +{"t_ms": 8626, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " **Automation & Subagent"}}} +{"t_ms": 8652, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "s**: Run concurrent task"}}} +{"t_ms": 8677, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "s"}}} +{"t_ms": 8703, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": ", terminal c"}}} +{"t_ms": 8728, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ommands, or "}}} +{"t_ms": 8754, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "delegate complex tasks t"}}} +{"t_ms": 8779, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "o subagents.\n* **Customi"}}} +{"t_ms": 8804, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "zations**: C"}}} +{"t_ms": 8830, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "onfigure [skills](file:/"}}} +{"t_ms": 8855, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "//slash-disab"}}} +{"t_ms": 8881, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "led-home/.gemini/antigra"}}} +{"t_ms": 8906, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "vity-cli/builtin/skills/"}}} +{"t_ms": 8931, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "agy-customiz"}}} +{"t_ms": 8957, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ations/SKILL.md), wor"}}} +{"t_ms": 8982, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "kspace rules"}}} +{"t_ms": 9007, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": ", hooks, and MCP servers"}}} +{"t_ms": 9033, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": ".\n\nLet me kn"}}} +{"t_ms": 9058, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ow what you would like t"}}} +{"t_ms": 9085, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "o work on!"}}} +{"t_ms": 9136, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 3.416356468, "usage": {"input_tokens": 13315, "output_tokens": 309, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 13624}}}} +{"t_ms": 9162, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "status": "SUCCESS", "response": "Here is a quick guide to using **Antigravity (AGY)** and its available slash commands:\n\n### Available Slash Commands\n\n* **/plan** – Initiates detailed step-by-step planning and architecture design before making code changes.\n* **/grill-me** – Starts an interactive interview/review session to refine requirements and architectural decisions.\n* **/goal** – Sets a high-level, long-running goal where the agent works autonomously and thoroughly until completion.\n* **/schedule** – Sets up one-shot timers or recurring cron jobs for periodic checks and background tasks.\n* **/browser** – Directs browser automation, testing, and web research workflows.\n* **/learn** – Teaches the agent preferences, project rules, or recurring setup procedures to persist across future sessions.\n\n---\n\n### What I Can Help You With\n\n* **Code Exploration & Editing**: Search codebase, view/edit files, run tests, and debug issues.\n* **Refactoring & Feature Development**: Plan, implement, and verify multi-file features.\n* **Automation & Subagents**: Run concurrent tasks, terminal commands, or delegate complex tasks to subagents.\n* **Customizations**: Configure [skills](file:///.gemini/antigravity-cli/builtin/skills/agy-customizations/SKILL.md), workspace rules, hooks, and MCP servers.\n\nLet me know what you would like to work on!\n", "duration_seconds": 8.84160525, "num_turns": 1, "usage": {"input_tokens": 37027, "output_tokens": 575, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 37602}}}} diff --git a/plugins/antigravity-provider/test/fixtures/slash-enabled.capture.ndjson b/plugins/antigravity-provider/test/fixtures/slash-enabled.capture.ndjson new file mode 100644 index 0000000000..852d00e9cd --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/slash-enabled.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 483, "stream": "capture", "data": {"exit_code": 2}} diff --git a/plugins/antigravity-provider/test/fixtures/slash-enabled.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/slash-enabled.stderr.ndjson new file mode 100644 index 0000000000..cf7c668f40 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/slash-enabled.stderr.ndjson @@ -0,0 +1 @@ +{"t_ms": 375, "stream": "stderr", "data": "error: /help is answered by the CLI itself and is unavailable with --input-format stream-json; run it as its own --print /help invocation"} diff --git a/plugins/antigravity-provider/test/fixtures/slash-enabled.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/slash-enabled.stdin.ndjson new file mode 100644 index 0000000000..73da2a88d2 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/slash-enabled.stdin.ndjson @@ -0,0 +1 @@ +{"t_ms": 374, "stream": "stdin", "data": {"event": "user", "message": {"content": "/help"}}} diff --git a/plugins/antigravity-provider/test/fixtures/slash-enabled.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/slash-enabled.stdout.ndjson new file mode 100644 index 0000000000..072ae3c437 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/slash-enabled.stdout.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 374, "stream": "stdout", "data": {"event": "init", "conversation_id": "ca3eefcf-2a10-470a-ac64-81f29d305c40", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "request-review"}}} +{"t_ms": 375, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "ca3eefcf-2a10-470a-ac64-81f29d305c40", "status": "ERROR", "response": "", "error": "/help is answered by the CLI itself and is unavailable with --input-format stream-json; run it as its own --print /help invocation", "duration_seconds": 0, "num_turns": 0, "usage": {"input_tokens": 0, "output_tokens": 0, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 0}}}} diff --git a/plugins/antigravity-provider/test/fixtures/state-inspection.ndjson b/plugins/antigravity-provider/test/fixtures/state-inspection.ndjson new file mode 100644 index 0000000000..2155c5cd9f --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/state-inspection.ndjson @@ -0,0 +1,6 @@ +{"stream": "inspection", "data": {"command": "python3 /finalize.py", "sha256": "574b0234656c44564f3ff3b013225c86f14ae79b67d0f3b45f6aaf1b1625440e", "version_fixture": "version.stdout.ndjson:1", "operation": "hashlib.sha256( bytes)"}} +{"stream": "inspection", "data": {"command": "HOME= strace -f -e trace=file -o /files.trace /.local/opt/agy/1.2.13/agy --input-format stream-json --output-format stream-json", "accessed_state_paths": ["/.gemini/antigravity-cli/antigravity-oauth-token", "/.gemini/antigravity-cli/conversation_summaries.db", "/.gemini/antigravity-cli/conversations", "/.gemini/antigravity-cli/mcp_config.json", "/.gemini/antigravity-cli/secplugins/config.json", "/.gemini/antigravity-cli/settings.json", "/.gemini/antigravity/conversations", "/.gemini/config/config.json", "/.gemini/config/mcp_config.json"], "outside_isolated_home_gemini_accesses": []}} +{"stream": "inspection", "data": {"command": "python3 /finalize.py", "database": "/.gemini/antigravity-cli/conversation_summaries.db", "query": "SELECT conversation_id,title,preview,step_count,last_modified_time,workspace_uris,project_id,agent_name,parent_conversation_id,app_data_dir FROM conversation_summaries", "rows": [{"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "title": "Secret Word Arithmetic Calculation", "preview": "Secret Word Arithmetic Calculation", "step_count": 13, "last_modified_time": "2026-09-29 18:42:36.36116556+00:00", "workspace_uris": "[\"file://\"]", "project_id": "default-cli-project", "agent_name": "", "parent_conversation_id": "", "app_data_dir": "antigravity-cli"}]}} +{"stream": "inspection", "data": {"database": "/.gemini/antigravity-cli/conversations/b966b16a-caf4-448a-bb24-0e34956022b0.db", "query": "SELECT name FROM sqlite_master WHERE type='table'", "tables": ["trajectory_meta", "steps", "gen_metadata", "executor_metadata", "parent_references", "trajectory_metadata_blob", "battle_mode_infos"], "steps_count": 13, "note": "Step payloads and metadata are binary blobs; no transcript decoder verified."}} +{"stream": "inspection", "data": {"command": "python3 /finalize.py", "workspace_files": {"sandbox.txt": "PHASE0_EDIT\n", "skip-permissions.txt": "PHASE0_EDIT\n", "plan.txt": "PHASE0_EDIT\n", "accept-edits.txt": "PHASE0_EDIT\n", "default.txt": "PHASE0_EDIT\n", "input.txt": "PHASE0_READ_MARKER\n"}}} +{"stream": "inspection", "data": {"command": "firecrawl scrape --format markdown", "retrieved_manifest": "```json\n{\n \"version\": \"1.2.12\",\n \"url\": \"https://storage.googleapis.com/antigravity-public/antigravity-cli/1.2.12-5784551402897408/linux-x64/cli_linux_x64.tar.gz\",\n \"sha512\": \"d5f0fe7433cb7c43ea878c07627a4fdb82d218f3bef5e6436266f5d9fdd2df145523453b9be0c4250391a64a007f5f42f7faff797bc2b2d502e7efb4874e383a\"\n}\n\n```\n", "note": "Fetch returned 1.2.12; supplied binary reports 1.2.13. Release provenance is not established by this manifest response."}} diff --git a/plugins/antigravity-provider/test/fixtures/stdin-close-mid-turn.capture.ndjson b/plugins/antigravity-provider/test/fixtures/stdin-close-mid-turn.capture.ndjson new file mode 100644 index 0000000000..be57e05c35 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/stdin-close-mid-turn.capture.ndjson @@ -0,0 +1,3 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", "", "--disable-slash-commands", "--dangerously-skip-permissions"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 319, "stream": "capture", "data": {"stdin_close": true}} +{"t_ms": 2576, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/stdin-close-mid-turn.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/stdin-close-mid-turn.stderr.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/stdin-close-mid-turn.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/stdin-close-mid-turn.stdin.ndjson new file mode 100644 index 0000000000..de27d4d5db --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/stdin-close-mid-turn.stdin.ndjson @@ -0,0 +1 @@ +{"t_ms": 292, "stream": "stdin", "data": {"event": "user", "message": {"content": "Reply with PHASE0_EOF. No tools."}}} diff --git a/plugins/antigravity-provider/test/fixtures/stdin-close-mid-turn.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/stdin-close-mid-turn.stdout.ndjson new file mode 100644 index 0000000000..88c41c8b1d --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/stdin-close-mid-turn.stdout.ndjson @@ -0,0 +1,5 @@ +{"t_ms": 292, "stream": "stdout", "data": {"event": "init", "conversation_id": "4a95b7fd-ae73-4b99-9c00-25d7c37f0250", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "always-proceed"}}} +{"t_ms": 319, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "4a95b7fd-ae73-4b99-9c00-25d7c37f0250", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 2230, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "4a95b7fd-ae73-4b99-9c00-25d7c37f0250", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "PHASE0_EOF"}}} +{"t_ms": 2459, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "4a95b7fd-ae73-4b99-9c00-25d7c37f0250", "step_index": 1, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 2.123780285, "usage": {"input_tokens": 11253, "output_tokens": 80, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11333}}}} +{"t_ms": 2459, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "4a95b7fd-ae73-4b99-9c00-25d7c37f0250", "status": "SUCCESS", "response": "PHASE0_EOF\n", "duration_seconds": 2.146186078, "num_turns": 1, "usage": {"input_tokens": 11253, "output_tokens": 80, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11333}}}} diff --git a/plugins/antigravity-provider/test/fixtures/stdin-mid-turn.capture.ndjson b/plugins/antigravity-provider/test/fixtures/stdin-mid-turn.capture.ndjson new file mode 100644 index 0000000000..12fb5138b4 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/stdin-mid-turn.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", "", "--disable-slash-commands", "--dangerously-skip-permissions"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 9564, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/stdin-mid-turn.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/stdin-mid-turn.stderr.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/stdin-mid-turn.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/stdin-mid-turn.stdin.ndjson new file mode 100644 index 0000000000..326c02c7d0 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/stdin-mid-turn.stdin.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 971, "stream": "stdin", "data": {"event": "user", "message": {"content": "Write a 500-word essay about primes. No tools."}}} +{"t_ms": 997, "stream": "stdin", "data": {"event": "user", "message": {"content": "Reply with exactly PHASE0_QUEUED. No tools."}}} diff --git a/plugins/antigravity-provider/test/fixtures/stdin-mid-turn.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/stdin-mid-turn.stdout.ndjson new file mode 100644 index 0000000000..bf80956e6a --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/stdin-mid-turn.stdout.ndjson @@ -0,0 +1,179 @@ +{"t_ms": 971, "stream": "stdout", "data": {"event": "init", "conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "always-proceed"}}} +{"t_ms": 997, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 3035, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "### The Indivisible Atom"}}} +{"t_ms": 3061, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "s of Arithme"}}} +{"t_ms": 3087, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "tic\n\nIn the vast landsca"}}} +{"t_ms": 3112, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "pe of mathem"}}} +{"t_ms": 3137, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "atics, prime numbers sta"}}} +{"t_ms": 3162, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "nd as"}}} +{"t_ms": 3213, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " fundamental building bl"}}} +{"t_ms": 3238, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ocks—the i"}}} +{"t_ms": 3263, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ndivisible atoms of arit"}}} +{"t_ms": 3289, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "hmetic. Defi"}}} +{"t_ms": 3314, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ned simply as integers g"}}} +{"t_ms": 3339, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "reater than "}}} +{"t_ms": 3365, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "one that are divisible o"}}} +{"t_ms": 3390, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "nly by one"}}} +{"t_ms": 3441, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " and themselves, primes possess an e"}}} +{"t_ms": 3466, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "lemental pur"}}} +{"t_ms": 3491, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ity. Yet beneath this de"}}} +{"t_ms": 3517, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ceptive simplicity lies "}}} +{"t_ms": 3542, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "one of the d"}}} +{"t_ms": 3567, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "eepest, most beautiful m"}}} +{"t_ms": 3592, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ysteries in "}}} +{"t_ms": 3618, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "human"}}} +{"t_ms": 3668, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " inquiry: an"}}} +{"t_ms": 3694, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " unending dance between "}}} +{"t_ms": 3719, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "order and ch"}}} +{"t_ms": 3744, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "aos, predictability and "}}} +{"t_ms": 3770, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "randomness.\n"}}} +{"t_ms": 3795, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "\nThe fascination with pr"}}} +{"t_ms": 3820, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "imes stretches back to antiquity. In"}}} +{"t_ms": 3846, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " approximate"}}} +{"t_ms": 3871, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ly 300 BCE, the ancient "}}} +{"t_ms": 3897, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "Greek mathem"}}} +{"t_ms": 3922, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "atician Euclid formulate"}}} +{"t_ms": 3947, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "d one of history’s mos"}}} +{"t_ms": 3972, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "t elegant proofs in his "}}} +{"t_ms": 3998, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "*Elements*, demonstratin"}}} +{"t_ms": 4023, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "g that the s"}}} +{"t_ms": 4048, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "upply of prime numbers i"}}} +{"t_ms": 4074, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "s infinite. "}}} +{"t_ms": 4099, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "If one assumes a finite "}}} +{"t_ms": 4124, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "list of primes, multiply"}}} +{"t_ms": 4150, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ing them tog"}}} +{"t_ms": 4175, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ether and adding one yields a new nu"}}} +{"t_ms": 4201, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "mber that is"}}} +{"t_ms": 4226, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " either prime itself or "}}} +{"t_ms": 4251, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "divisible by"}}} +{"t_ms": 4277, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " a prime not on the orig"}}} +{"t_ms": 4303, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "inal list. Euclid’s pr"}}} +{"t_ms": 4328, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "oof establis"}}} +{"t_ms": 4354, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "hed an eternal truth: no matter how "}}} +{"t_ms": 4379, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "far we trave"}}} +{"t_ms": 4404, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "l along the number line,"}}} +{"t_ms": 4430, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " new primes will continu"}}} +{"t_ms": 4455, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "e to emerge."}}} +{"t_ms": 4481, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "\n\nDespite their infinite"}}} +{"t_ms": 4583, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " nature, primes become i"}}} +{"t_ms": 4608, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ncreasingly sparse as nu"}}} +{"t_ms": 4633, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "mbers grow l"}}} +{"t_ms": 4659, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "arger. For centuries, th"}}} +{"t_ms": 4684, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "eir distribu"}}} +{"t_ms": 4709, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "tion appeared entirely e"}}} +{"t_ms": 4734, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "rratic, defying simple formulas. It "}}} +{"t_ms": 4760, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "was not unti"}}} +{"t_ms": 4785, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "l the nineteenth century"}}} +{"t_ms": 4810, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " that Carl F"}}} +{"t_ms": 4836, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "riedrich Gauss and Adrie"}}} +{"t_ms": 4861, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "n-Marie Legendre conject"}}} +{"t_ms": 4886, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ured the Pri"}}} +{"t_ms": 4912, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "me Number Theorem, later"}}} +{"t_ms": 4937, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " proven in 1896 by Jacqu"}}} +{"t_ms": 4963, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "es Hadamard and Charles "}}} +{"t_ms": 4988, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "Jean de la V"}}} +{"t_ms": 5013, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "allée Poussin. The theo"}}} +{"t_ms": 5038, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "rem revealed a breathtak"}}} +{"t_ms": 5064, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ing macrosco"}}} +{"t_ms": 5089, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "pic regularity: the probabi"}}} +{"t_ms": 5114, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "lity that a "}}} +{"t_ms": 5140, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "randomly chosen integer "}}} +{"t_ms": 5165, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "$N$ is prime is inversel"}}} +{"t_ms": 5190, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "y proportion"}}} +{"t_ms": 5216, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "al to its natural logari"}}} +{"t_ms": 5241, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "thm, $\\ln"}}} +{"t_ms": 5292, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "(N)$. While individual p"}}} +{"t_ms": 5317, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "rimes appear"}}} +{"t_ms": 5343, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " almost randomly, their "}}} +{"t_ms": 5368, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "collective b"}}} +{"t_ms": 5393, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ehavior follows a remark"}}} +{"t_ms": 5418, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ably smooth "}}} +{"t_ms": 5444, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "logarithmic curve.\n\nThis"}}} +{"t_ms": 5469, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " duality between"}}} +{"t_ms": 5495, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " local chaos and global "}}} +{"t_ms": 5520, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "harmony is epitomized by"}}} +{"t_ms": 5546, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " the Riemann"}}} +{"t_ms": 5571, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " Hypothesis, proposed by"}}} +{"t_ms": 5596, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " Bernhard Riemann in 185"}}} +{"t_ms": 5622, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "9. By"}}} +{"t_ms": 5673, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " extending t"}}} +{"t_ms": 5698, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "he zeta function into th"}}} +{"t_ms": 5723, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "e complex pl"}}} +{"t_ms": 5749, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ane, Riemann connected t"}}} +{"t_ms": 5774, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "he distribution of prime"}}} +{"t_ms": 5799, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "s directly t"}}} +{"t_ms": 5825, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "o the non-trivial zeros "}}} +{"t_ms": 5850, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "of this func"}}} +{"t_ms": 5875, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "tion. Widely regarded as the most"}}} +{"t_ms": 5900, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " famous unsolved problem"}}} +{"t_ms": 5926, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " in mathemat"}}} +{"t_ms": 5952, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ics, the Riemann Hypothe"}}} +{"t_ms": 5977, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "sis asserts "}}} +{"t_ms": 6002, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "that all these non-trivi"}}} +{"t_ms": 6028, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "al zeros lie"}}} +{"t_ms": 6053, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " on a"}}} +{"t_ms": 6104, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " single critical line. I"}}} +{"t_ms": 6129, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "f true, it would unlock "}}} +{"t_ms": 6154, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "a profound h"}}} +{"t_ms": 6180, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "armonic symphony governi"}}} +{"t_ms": 6205, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ng the exact fluctuation"}}} +{"t_ms": 6230, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "s of prime n"}}} +{"t_ms": 6256, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "umbers.\n\nBeyond"}}} +{"t_ms": 6332, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " their aesthetic allure in pure math"}}} +{"t_ms": 6357, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ematics, pri"}}} +{"t_ms": 6383, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "me numbers have quietly "}}} +{"t_ms": 6408, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "become the b"}}} +{"t_ms": 6434, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "edrock of modern technological civil"}}} +{"t_ms": 6459, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ization. In "}}} +{"t_ms": 6485, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "the 1970s, the developme"}}} +{"t_ms": 6510, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "nt of asymmetric public-"}}} +{"t_ms": 6535, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "key cryptogr"}}} +{"t_ms": 6561, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "aphy—most notably the "}}} +{"t_ms": 6586, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "RSA algorithm—transfor"}}} +{"t_ms": 6611, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "med prime numbers into i"}}} +{"t_ms": 6637, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ndispensable digital gua"}}} +{"t_ms": 6662, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "rdians. The "}}} +{"t_ms": 6687, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "security of global comme"}}} +{"t_ms": 6713, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "rce, secure "}}} +{"t_ms": 6738, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "communication, and onlin"}}} +{"t_ms": 6763, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "e privacy re"}}} +{"t_ms": 6789, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "sts upon a computational asymmetry: "}}} +{"t_ms": 6814, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "multiplying two large pr"}}} +{"t_ms": 6839, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ime numbers "}}} +{"t_ms": 6865, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "together is nearly insta"}}} +{"t_ms": 6890, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ntaneous, bu"}}} +{"t_ms": 6915, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "t factoring their astron"}}} +{"t_ms": 6941, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "omical product back into"}}} +{"t_ms": 6966, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " its constit"}}} +{"t_ms": 6991, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "uent primes is virtually"}}} +{"t_ms": 7017, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " impossible "}}} +{"t_ms": 7042, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "with classical computing resources.\n"}}} +{"t_ms": 7067, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "\nPrime numbe"}}} +{"t_ms": 7093, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "rs represent a sublime i"}}} +{"t_ms": 7118, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ntersection of philosoph"}}} +{"t_ms": 7143, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "y, abstract "}}} +{"t_ms": 7169, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "theory, and practical ut"}}} +{"t_ms": 7194, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ility. They "}}} +{"t_ms": 7219, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "remind us that the most "}}} +{"t_ms": 7245, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "basic structures in natu"}}} +{"t_ms": 7270, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "re often harbor the deep"}}} +{"t_ms": 7295, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "est questions. As mathem"}}} +{"t_ms": 7320, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "aticians con"}}} +{"t_ms": 7346, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "tinue to explore unresol"}}} +{"t_ms": 7371, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ved enigmas "}}} +{"t_ms": 7397, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "like the Twin Prime Conjecture or th"}}} +{"t_ms": 7422, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "e Goldbach Conjecture, p"}}} +{"t_ms": 7448, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "rimes remain"}}} +{"t_ms": 7473, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " an enduring testament t"}}} +{"t_ms": 7498, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "o the infini"}}} +{"t_ms": 7524, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "te richness waiting to b"}}} +{"t_ms": 7549, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "e uncovered within the s"}}} +{"t_ms": 7574, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "implest conc"}}} +{"t_ms": 7600, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "epts."}}} +{"t_ms": 7650, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 6.647623906, "usage": {"input_tokens": 11252, "output_tokens": 639, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11891}}}} +{"t_ms": 7650, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "status": "SUCCESS", "response": "### The Indivisible Atoms of Arithmetic\n\nIn the vast landscape of mathematics, prime numbers stand as fundamental building blocks—the indivisible atoms of arithmetic. Defined simply as integers greater than one that are divisible only by one and themselves, primes possess an elemental purity. Yet beneath this deceptive simplicity lies one of the deepest, most beautiful mysteries in human inquiry: an unending dance between order and chaos, predictability and randomness.\n\nThe fascination with primes stretches back to antiquity. In approximately 300 BCE, the ancient Greek mathematician Euclid formulated one of history’s most elegant proofs in his *Elements*, demonstrating that the supply of prime numbers is infinite. If one assumes a finite list of primes, multiplying them together and adding one yields a new number that is either prime itself or divisible by a prime not on the original list. Euclid’s proof established an eternal truth: no matter how far we travel along the number line, new primes will continue to emerge.\n\nDespite their infinite nature, primes become increasingly sparse as numbers grow larger. For centuries, their distribution appeared entirely erratic, defying simple formulas. It was not until the nineteenth century that Carl Friedrich Gauss and Adrien-Marie Legendre conjectured the Prime Number Theorem, later proven in 1896 by Jacques Hadamard and Charles Jean de la Vallée Poussin. The theorem revealed a breathtaking macroscopic regularity: the probability that a randomly chosen integer $N$ is prime is inversely proportional to its natural logarithm, $\\ln(N)$. While individual primes appear almost randomly, their collective behavior follows a remarkably smooth logarithmic curve.\n\nThis duality between local chaos and global harmony is epitomized by the Riemann Hypothesis, proposed by Bernhard Riemann in 1859. By extending the zeta function into the complex plane, Riemann connected the distribution of primes directly to the non-trivial zeros of this function. Widely regarded as the most famous unsolved problem in mathematics, the Riemann Hypothesis asserts that all these non-trivial zeros lie on a single critical line. If true, it would unlock a profound harmonic symphony governing the exact fluctuations of prime numbers.\n\nBeyond their aesthetic allure in pure mathematics, prime numbers have quietly become the bedrock of modern technological civilization. In the 1970s, the development of asymmetric public-key cryptography—most notably the RSA algorithm—transformed prime numbers into indispensable digital guardians. The security of global commerce, secure communication, and online privacy rests upon a computational asymmetry: multiplying two large prime numbers together is nearly instantaneous, but factoring their astronomical product back into its constituent primes is virtually impossible with classical computing resources.\n\nPrime numbers represent a sublime intersection of philosophy, abstract theory, and practical utility. They remind us that the most basic structures in nature often harbor the deepest questions. As mathematicians continue to explore unresolved enigmas like the Twin Prime Conjecture or the Goldbach Conjecture, primes remain an enduring testament to the infinite richness waiting to be uncovered within the simplest concepts.\n", "duration_seconds": 6.674385393, "num_turns": 1, "usage": {"input_tokens": 11252, "output_tokens": 639, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11891}}}} +{"t_ms": 7677, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 2, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 9187, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "PHASE"}}} +{"t_ms": 9213, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "0_QUEUED"}}} +{"t_ms": 9466, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 3, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 1.793731176, "usage": {"input_tokens": 11913, "output_tokens": 6, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11919}}}} +{"t_ms": 9466, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "status": "SUCCESS", "response": "PHASE0_QUEUED\n", "duration_seconds": 8.483804893, "num_turns": 2, "usage": {"input_tokens": 23165, "output_tokens": 645, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 23810}}}} diff --git a/plugins/antigravity-provider/test/fixtures/subagent.capture.ndjson b/plugins/antigravity-provider/test/fixtures/subagent.capture.ndjson new file mode 100644 index 0000000000..854c3c3eba --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/subagent.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", "", "--dangerously-skip-permissions"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 13163, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/subagent.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/subagent.stderr.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/subagent.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/subagent.stdin.ndjson new file mode 100644 index 0000000000..0814f42805 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/subagent.stdin.ndjson @@ -0,0 +1 @@ +{"t_ms": 291, "stream": "stdin", "data": {"event": "user", "message": {"content": "Use define_subagent to define a small subagent named phase0-counter that answers arithmetic only, then invoke_subagent to ask it 2+2. Report its answer. Do not use shell."}}} diff --git a/plugins/antigravity-provider/test/fixtures/subagent.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/subagent.stdout.ndjson new file mode 100644 index 0000000000..cec633cda7 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/subagent.stdout.ndjson @@ -0,0 +1,23 @@ +{"t_ms": 291, "stream": "stdout", "data": {"event": "init", "conversation_id": "eceffa7f-bbef-47d0-a3cf-3daeaf20a829", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "always-proceed"}}} +{"t_ms": 319, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "eceffa7f-bbef-47d0-a3cf-3daeaf20a829", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 3924, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "eceffa7f-bbef-47d0-a3cf-3daeaf20a829", "step_index": 1, "state": "DONE", "step_type": "agent_response", "duration_seconds": 3.580501478, "usage": {"input_tokens": 11252, "output_tokens": 188, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11440}}}} +{"t_ms": 3924, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "eceffa7f-bbef-47d0-a3cf-3daeaf20a829", "step_index": 2, "state": "ACTIVE", "step_type": "tool", "tool_name": "define_subagent", "tool_info": {"name": "define_subagent", "parameters": {"description": "A subagent specialized in answering arithmetic questions only.", "name": "phase0-counter"}}}}} +{"t_ms": 3924, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "eceffa7f-bbef-47d0-a3cf-3daeaf20a829", "step_index": 2, "state": "DONE", "step_type": "tool", "tool_name": "define_subagent", "duration_seconds": 0.003321972, "tool_info": {"name": "define_subagent", "parameters": {"description": "A subagent specialized in answering arithmetic questions only.", "name": "phase0-counter"}, "output": "Subagent \"phase0-counter\" defined successfully. It can now be invoked via invoke_subagent."}}}} +{"t_ms": 6541, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "eceffa7f-bbef-47d0-a3cf-3daeaf20a829", "step_index": 3, "state": "DONE", "step_type": "agent_response", "duration_seconds": 2.634297625, "usage": {"input_tokens": 11433, "output_tokens": 79, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11512}}}} +{"t_ms": 6541, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "eceffa7f-bbef-47d0-a3cf-3daeaf20a829", "step_index": 4, "state": "ACTIVE", "step_type": "subagent", "tool_name": "invoke_subagent", "subagent_info": {"subagents": [{"type_name": "phase0-counter", "role": "Arithmetic Assistant", "initial_prompt": "What is 2+2?"}]}}}} +{"t_ms": 6565, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "eceffa7f-bbef-47d0-a3cf-3daeaf20a829", "step_index": 4, "state": "DONE", "step_type": "subagent", "tool_name": "invoke_subagent", "duration_seconds": 0.01034356, "subagent_info": {"subagents": [{"type_name": "phase0-counter", "role": "Arithmetic Assistant", "initial_prompt": "What is 2+2?", "conversation_id": "b6f02329-7415-49a7-a9a9-1cd09e898d26", "log_uri": "file:///.gemini/antigravity-cli/brain/b6f02329-7415-49a7-a9a9-1cd09e898d26/.system_generated/logs/transcript.jsonl", "workspace_uris": ["file://"]}]}}}} +{"t_ms": 8527, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "eceffa7f-bbef-47d0-a3cf-3daeaf20a829", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "I"}}} +{"t_ms": 8679, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "eceffa7f-bbef-47d0-a3cf-3daeaf20a829", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " have define"}}} +{"t_ms": 8704, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "eceffa7f-bbef-47d0-a3cf-3daeaf20a829", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "d the subagent `phase0-c"}}} +{"t_ms": 8730, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "eceffa7f-bbef-47d0-a3cf-3daeaf20a829", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ounter` and "}}} +{"t_ms": 8755, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "eceffa7f-bbef-47d0-a3cf-3daeaf20a829", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "invoked it with the ques"}}} +{"t_ms": 8780, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "eceffa7f-bbef-47d0-a3cf-3daeaf20a829", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "tion \"What"}}} +{"t_ms": 8806, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "eceffa7f-bbef-47d0-a3cf-3daeaf20a829", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " is 2+2?\". Awaiting its "}}} +{"t_ms": 8831, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "eceffa7f-bbef-47d0-a3cf-3daeaf20a829", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "response."}}} +{"t_ms": 9135, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "eceffa7f-bbef-47d0-a3cf-3daeaf20a829", "step_index": 5, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 2.58161986, "usage": {"input_tokens": 11739, "output_tokens": 31, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11770}}}} +{"t_ms": 9715, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "eceffa7f-bbef-47d0-a3cf-3daeaf20a829", "step_index": 6, "state": "DONE", "step_type": "system_message", "duration_seconds": 3.6749e-05}}} +{"t_ms": 12484, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "eceffa7f-bbef-47d0-a3cf-3daeaf20a829", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "The `phase0-"}}} +{"t_ms": 12510, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "eceffa7f-bbef-47d0-a3cf-3daeaf20a829", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "counter` sub"}}} +{"t_ms": 12610, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "eceffa7f-bbef-47d0-a3cf-3daeaf20a829", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "agent answered: **4**."}}} +{"t_ms": 13042, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "eceffa7f-bbef-47d0-a3cf-3daeaf20a829", "step_index": 7, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 3.329676969, "usage": {"input_tokens": 11886, "output_tokens": 74, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11960}}}} +{"t_ms": 13068, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "eceffa7f-bbef-47d0-a3cf-3daeaf20a829", "status": "SUCCESS", "response": "I have defined the subagent `phase0-counter` and invoked it with the question \"What is 2+2?\". Awaiting its response.\nThe `phase0-counter` subagent answered: **4**.\n", "duration_seconds": 12.746450429, "num_turns": 1, "usage": {"input_tokens": 46310, "output_tokens": 372, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 46682}}}} diff --git a/plugins/antigravity-provider/test/fixtures/text-block.capture.ndjson b/plugins/antigravity-provider/test/fixtures/text-block.capture.ndjson new file mode 100644 index 0000000000..b1f7ab6455 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/text-block.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", ""], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 4102, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/text-block.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/text-block.stderr.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/text-block.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/text-block.stdin.ndjson new file mode 100644 index 0000000000..faeb98b247 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/text-block.stdin.ndjson @@ -0,0 +1 @@ +{"t_ms": 2020, "stream": "stdin", "data": {"event": "user", "message": {"content": [{"type": "text", "text": "Reply with PHASE0_BLOCK. No tools."}]}}} diff --git a/plugins/antigravity-provider/test/fixtures/text-block.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/text-block.stdout.ndjson new file mode 100644 index 0000000000..ee7a7ec32a --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/text-block.stdout.ndjson @@ -0,0 +1,5 @@ +{"t_ms": 2020, "stream": "stdout", "data": {"event": "init", "conversation_id": "0757bec3-d263-4f90-bf34-7510d638d8c5", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "request-review"}}} +{"t_ms": 2051, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0757bec3-d263-4f90-bf34-7510d638d8c5", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 3939, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0757bec3-d263-4f90-bf34-7510d638d8c5", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "PHASE0_BLOCK"}}} +{"t_ms": 3990, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0757bec3-d263-4f90-bf34-7510d638d8c5", "step_index": 1, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 1.932265932, "usage": {"input_tokens": 11235, "output_tokens": 62, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11297}}}} +{"t_ms": 3990, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "0757bec3-d263-4f90-bf34-7510d638d8c5", "status": "SUCCESS", "response": "PHASE0_BLOCK\n", "duration_seconds": 1.959296182, "num_turns": 1, "usage": {"input_tokens": 11235, "output_tokens": 62, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11297}}}} diff --git a/plugins/antigravity-provider/test/fixtures/text-turn.capture.ndjson b/plugins/antigravity-provider/test/fixtures/text-turn.capture.ndjson new file mode 100644 index 0000000000..1b006f8ab9 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/text-turn.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", ""], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 2911, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/text-turn.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/text-turn.stderr.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/text-turn.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/text-turn.stdin.ndjson new file mode 100644 index 0000000000..389b97dece --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/text-turn.stdin.ndjson @@ -0,0 +1 @@ +{"t_ms": 294, "stream": "stdin", "data": {"event": "user", "message": {"role": "user", "content": "Reply with exactly PHASE0_HELLO. No tools."}}} diff --git a/plugins/antigravity-provider/test/fixtures/text-turn.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/text-turn.stdout.ndjson new file mode 100644 index 0000000000..89fc8a7677 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/text-turn.stdout.ndjson @@ -0,0 +1,5 @@ +{"t_ms": 294, "stream": "stdout", "data": {"event": "init", "conversation_id": "e5cf1e2d-c715-4328-8e67-4a85c8dc3cda", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "request-review"}}} +{"t_ms": 351, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e5cf1e2d-c715-4328-8e67-4a85c8dc3cda", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 2544, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e5cf1e2d-c715-4328-8e67-4a85c8dc3cda", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "PHASE0_HELLO"}}} +{"t_ms": 2746, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e5cf1e2d-c715-4328-8e67-4a85c8dc3cda", "step_index": 1, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 2.413995705, "usage": {"input_tokens": 11235, "output_tokens": 78, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11313}}}} +{"t_ms": 2772, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "e5cf1e2d-c715-4328-8e67-4a85c8dc3cda", "status": "SUCCESS", "response": "PHASE0_HELLO\n", "duration_seconds": 2.445370438, "num_turns": 1, "usage": {"input_tokens": 11235, "output_tokens": 78, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11313}}}} diff --git a/plugins/antigravity-provider/test/fixtures/tools-accept-edits.capture.ndjson b/plugins/antigravity-provider/test/fixtures/tools-accept-edits.capture.ndjson new file mode 100644 index 0000000000..d7c50c17a4 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/tools-accept-edits.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", "", "--disable-slash-commands", "--mode", "accept-edits"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 16127, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/tools-accept-edits.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/tools-accept-edits.stderr.ndjson new file mode 100644 index 0000000000..30cf90e622 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/tools-accept-edits.stderr.ndjson @@ -0,0 +1 @@ +{"t_ms": 15954, "stream": "stderr", "data": "jetski: no output produced — a tool required the \"command\" permission that headless mode cannot prompt for, so it was auto-denied. Add an allow-rule under permissions.allow in settings.json (e.g. command()). Alternatively, re-run with --dangerously-skip-permissions to auto-approve all tools."} diff --git a/plugins/antigravity-provider/test/fixtures/tools-accept-edits.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/tools-accept-edits.stdin.ndjson new file mode 100644 index 0000000000..4d89737919 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/tools-accept-edits.stdin.ndjson @@ -0,0 +1,3 @@ +{"t_ms": 288, "stream": "stdin", "data": {"event": "user", "message": {"role": "user", "content": "Use view_file to read /input.txt. Report its contents. Do not use a shell."}}} +{"t_ms": 5150, "stream": "stdin", "data": {"event": "user", "message": {"role": "user", "content": "Use write_to_file to create /accept-edits.txt with PHASE0_WRITE. Then use replace_file_content to change PHASE0_WRITE to PHASE0_EDIT. No shell. Do not ask me questions."}}} +{"t_ms": 13611, "stream": "stdin", "data": {"event": "user", "message": {"role": "user", "content": "Use run_command exactly once to run printf PHASE0_SHELL in . Report the output. Do not ask me questions."}}} diff --git a/plugins/antigravity-provider/test/fixtures/tools-accept-edits.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/tools-accept-edits.stdout.ndjson new file mode 100644 index 0000000000..57d50ad776 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/tools-accept-edits.stdout.ndjson @@ -0,0 +1,40 @@ +{"t_ms": 288, "stream": "stdout", "data": {"event": "init", "conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "request-review"}}} +{"t_ms": 315, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 2807, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 1, "state": "DONE", "step_type": "agent_response", "duration_seconds": 2.468344855, "usage": {"input_tokens": 11272, "output_tokens": 149, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11421}}}} +{"t_ms": 2807, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 2, "state": "ACTIVE", "step_type": "tool", "tool_name": "view_file", "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/input.txt"}}}}} +{"t_ms": 2807, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 2, "state": "DONE", "step_type": "tool", "tool_name": "view_file", "duration_seconds": 0.002720597, "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/input.txt"}, "output": "2 lines, 19 bytes"}}}} +{"t_ms": 4820, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "The contents"}}} +{"t_ms": 4870, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " of [/workspa"}}} +{"t_ms": 4896, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ce/input.txt"}}} +{"t_ms": 4921, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "](file:///tmp"}}} +{"t_ms": 4972, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "/agy-p0/work"}}} +{"t_ms": 4998, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "space/input.txt) are:\n\n```\nPHASE0_RE"}}} +{"t_ms": 5023, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "AD_MARKER\n``"}}} +{"t_ms": 5048, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "`"}}} +{"t_ms": 5150, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 3, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 2.360276704, "usage": {"input_tokens": 11494, "output_tokens": 47, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11541}}}} +{"t_ms": 5150, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "status": "SUCCESS", "response": "The contents of [/input.txt](file:///input.txt) are:\n\n```\nPHASE0_READ_MARKER\n```\n", "duration_seconds": 4.857244426, "num_turns": 1, "usage": {"input_tokens": 22766, "output_tokens": 196, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 22962}}}} +{"t_ms": 5176, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 4, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 8771, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 5, "state": "DONE", "step_type": "agent_response", "duration_seconds": 3.600197654, "usage": {"input_tokens": 11663, "output_tokens": 419, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12082}}}} +{"t_ms": 8771, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 6, "state": "ACTIVE", "step_type": "tool", "tool_name": "write_to_file", "tool_info": {"name": "write_to_file", "parameters": {"TargetFile": "/accept-edits.txt"}}}}} +{"t_ms": 8771, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 6, "state": "DONE", "step_type": "tool", "tool_name": "write_to_file", "duration_seconds": 0.003051666, "tool_info": {"name": "write_to_file", "parameters": {"TargetFile": "/accept-edits.txt"}}}}} +{"t_ms": 11213, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 7, "state": "DONE", "step_type": "agent_response", "duration_seconds": 2.42873306, "usage": {"input_tokens": 11846, "output_tokens": 129, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11975}}}} +{"t_ms": 11213, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 8, "state": "ACTIVE", "step_type": "tool", "tool_name": "replace_file_content", "tool_info": {"name": "replace_file_content", "parameters": {"TargetFile": "/accept-edits.txt"}}}}} +{"t_ms": 11213, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 8, "state": "DONE", "step_type": "tool", "tool_name": "replace_file_content", "duration_seconds": 0.003025767, "tool_info": {"name": "replace_file_content", "parameters": {"TargetFile": "/accept-edits.txt"}}}}} +{"t_ms": 12799, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "The file"}}} +{"t_ms": 12900, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " [/"}}} +{"t_ms": 12926, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "accept-edits"}}} +{"t_ms": 12951, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": ".txt](file:///"}}} +{"t_ms": 13002, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "tmp/agy-p0/w"}}} +{"t_ms": 13027, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "orkspace/accept-edits.tx"}}} +{"t_ms": 13053, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "t) has been "}}} +{"t_ms": 13078, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "created with `PHASE0"}}} +{"t_ms": 13155, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "_WRITE` and then updated"}}} +{"t_ms": 13180, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " to `PHASE0_"}}} +{"t_ms": 13206, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "EDIT`."}}} +{"t_ms": 13611, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 9, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 2.385314949, "usage": {"input_tokens": 12152, "output_tokens": 59, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12211}}}} +{"t_ms": 13611, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "status": "SUCCESS", "response": "The file [/accept-edits.txt](file:///accept-edits.txt) has been created with `PHASE0_WRITE` and then updated to `PHASE0_EDIT`.\n", "duration_seconds": 13.300599177, "num_turns": 2, "usage": {"input_tokens": 58427, "output_tokens": 803, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 59230}}}} +{"t_ms": 13638, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 10, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 15928, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 11, "state": "DONE", "step_type": "agent_response", "duration_seconds": 2.302666458, "usage": {"input_tokens": 12310, "output_tokens": 98, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12408}}}} +{"t_ms": 15928, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 12, "state": "ACTIVE", "step_type": "tool", "tool_name": "run_command", "tool_info": {"name": "run_command", "parameters": {"CommandLine": "printf PHASE0_SHELL"}}}}} +{"t_ms": 15954, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 12, "state": "ERROR", "step_type": "tool", "tool_name": "run_command", "duration_seconds": 0.002216645, "tool_info": {"name": "run_command", "parameters": {"CommandLine": "printf PHASE0_SHELL"}, "error": {"type": "TOOL_ERROR", "message": "permission check failed for command \"printf PHASE0_SHELL\": user denied permission to run command:\nprintf PHASE0_SHELL\nDo not attempt to circumvent this denial by rephrasing the command, using alternative tools/scripts (e.g. python, sh, curl), or accessing the same target resource. Proceed without performing this action."}}}}} +{"t_ms": 15954, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "status": "SUCCESS", "response": "", "duration_seconds": 15.639601905, "num_turns": 3, "usage": {"input_tokens": 70737, "output_tokens": 901, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 71638}, "denied_actions": [{"action": "command", "display_name": "RunCommand"}]}}} diff --git a/plugins/antigravity-provider/test/fixtures/tools-default.capture.ndjson b/plugins/antigravity-provider/test/fixtures/tools-default.capture.ndjson new file mode 100644 index 0000000000..8c57e4e549 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/tools-default.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", "", "--disable-slash-commands"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 21620, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/tools-default.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/tools-default.stderr.ndjson new file mode 100644 index 0000000000..4e24b2cb59 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/tools-default.stderr.ndjson @@ -0,0 +1 @@ +{"t_ms": 21540, "stream": "stderr", "data": "jetski: no output produced — a tool required the \"command\" permission that headless mode cannot prompt for, so it was auto-denied. Add an allow-rule under permissions.allow in settings.json (e.g. command()). Alternatively, re-run with --dangerously-skip-permissions to auto-approve all tools."} diff --git a/plugins/antigravity-provider/test/fixtures/tools-default.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/tools-default.stdin.ndjson new file mode 100644 index 0000000000..371a7bda84 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/tools-default.stdin.ndjson @@ -0,0 +1,3 @@ +{"t_ms": 297, "stream": "stdin", "data": {"event": "user", "message": {"role": "user", "content": "Use view_file to read /input.txt. Report its contents. Do not use a shell."}}} +{"t_ms": 6781, "stream": "stdin", "data": {"event": "user", "message": {"role": "user", "content": "Use write_to_file to create /default.txt with PHASE0_WRITE. Then use replace_file_content to change PHASE0_WRITE to PHASE0_EDIT. No shell. Do not ask me questions."}}} +{"t_ms": 17022, "stream": "stdin", "data": {"event": "user", "message": {"role": "user", "content": "Use run_command exactly once to run printf PHASE0_SHELL in . Report the output. Do not ask me questions."}}} diff --git a/plugins/antigravity-provider/test/fixtures/tools-default.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/tools-default.stdout.ndjson new file mode 100644 index 0000000000..dedb01d707 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/tools-default.stdout.ndjson @@ -0,0 +1,41 @@ +{"t_ms": 297, "stream": "stdout", "data": {"event": "init", "conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "request-review"}}} +{"t_ms": 326, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 4079, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 1, "state": "DONE", "step_type": "agent_response", "duration_seconds": 3.738430576, "usage": {"input_tokens": 11257, "output_tokens": 149, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11406}}}} +{"t_ms": 4079, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 2, "state": "ACTIVE", "step_type": "tool", "tool_name": "view_file", "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/input.txt"}}}}} +{"t_ms": 4079, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 2, "state": "DONE", "step_type": "tool", "tool_name": "view_file", "duration_seconds": 0.002338062, "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/input.txt"}, "output": "2 lines, 19 bytes"}}}} +{"t_ms": 6096, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "The contents"}}} +{"t_ms": 6223, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " of [`/worksp"}}} +{"t_ms": 6248, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ace/input.tx"}}} +{"t_ms": 6274, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "t`](file:///tmp/agy"}}} +{"t_ms": 6350, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "-p0/workspace/input.txt) are:\n\n```te"}}} +{"t_ms": 6375, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "xt\nPHASE0_RE"}}} +{"t_ms": 6400, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "AD_MARKER"}}} +{"t_ms": 6451, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "\n```"}}} +{"t_ms": 6781, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 3, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 2.698156575, "usage": {"input_tokens": 11478, "output_tokens": 49, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11527}}}} +{"t_ms": 6781, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "status": "SUCCESS", "response": "The contents of [`/input.txt`](file:///input.txt) are:\n\n```text\nPHASE0_READ_MARKER\n```\n", "duration_seconds": 6.466229654, "num_turns": 1, "usage": {"input_tokens": 22735, "output_tokens": 198, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 22933}}}} +{"t_ms": 6807, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 4, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 10766, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 5, "state": "DONE", "step_type": "agent_response", "duration_seconds": 3.966197803, "usage": {"input_tokens": 11646, "output_tokens": 171, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11817}}}} +{"t_ms": 10766, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 6, "state": "ACTIVE", "step_type": "tool", "tool_name": "write_to_file", "tool_info": {"name": "write_to_file", "parameters": {"TargetFile": "/default.txt"}}}}} +{"t_ms": 10791, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 6, "state": "DONE", "step_type": "tool", "tool_name": "write_to_file", "duration_seconds": 0.003040074, "tool_info": {"name": "write_to_file", "parameters": {"TargetFile": "/default.txt"}}}}} +{"t_ms": 13910, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 7, "state": "DONE", "step_type": "agent_response", "duration_seconds": 3.136362179, "usage": {"input_tokens": 11813, "output_tokens": 116, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11929}}}} +{"t_ms": 13911, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 8, "state": "ACTIVE", "step_type": "tool", "tool_name": "replace_file_content", "tool_info": {"name": "replace_file_content", "parameters": {"TargetFile": "/default.txt"}}}}} +{"t_ms": 13911, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 8, "state": "DONE", "step_type": "tool", "tool_name": "replace_file_content", "duration_seconds": 0.003396671, "tool_info": {"name": "replace_file_content", "parameters": {"TargetFile": "/default.txt"}}}}} +{"t_ms": 15904, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "I"}}} +{"t_ms": 16032, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " have create"}}} +{"t_ms": 16057, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "d [`/workspac"}}} +{"t_ms": 16083, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "e/default.txt`](file:///"}}} +{"t_ms": 16159, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "tmp/agy-p0/w"}}} +{"t_ms": 16184, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "orkspace/default.txt) wi"}}} +{"t_ms": 16210, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "th `PHASE0_WRITE` and th"}}} +{"t_ms": 16235, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "en updated i"}}} +{"t_ms": 16261, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "t to"}}} +{"t_ms": 16286, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " `PHASE0_EDI"}}} +{"t_ms": 16312, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "T` using `replace_file_c"}}} +{"t_ms": 16337, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ontent`."}}} +{"t_ms": 17022, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 9, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 3.099484037, "usage": {"input_tokens": 12103, "output_tokens": 61, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12164}}}} +{"t_ms": 17022, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "status": "SUCCESS", "response": "I have created [`/default.txt`](file:///default.txt) with `PHASE0_WRITE` and then updated it to `PHASE0_EDIT` using `replace_file_content`.\n", "duration_seconds": 16.710782303, "num_turns": 2, "usage": {"input_tokens": 58297, "output_tokens": 546, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 58843}}}} +{"t_ms": 17049, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 10, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 21540, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 11, "state": "DONE", "step_type": "agent_response", "duration_seconds": 4.490021482, "usage": {"input_tokens": 12263, "output_tokens": 370, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12633}}}} +{"t_ms": 21540, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 12, "state": "ACTIVE", "step_type": "tool", "tool_name": "run_command", "tool_info": {"name": "run_command", "parameters": {"CommandLine": "printf PHASE0_SHELL"}}}}} +{"t_ms": 21540, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 12, "state": "ERROR", "step_type": "tool", "tool_name": "run_command", "duration_seconds": 0.002821636, "tool_info": {"name": "run_command", "parameters": {"CommandLine": "printf PHASE0_SHELL"}, "error": {"type": "TOOL_ERROR", "message": "permission check failed for command \"printf PHASE0_SHELL\": user denied permission to run command:\nprintf PHASE0_SHELL\nDo not attempt to circumvent this denial by rephrasing the command, using alternative tools/scripts (e.g. python, sh, curl), or accessing the same target resource. Proceed without performing this action."}}}}} +{"t_ms": 21540, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "status": "SUCCESS", "response": "", "duration_seconds": 21.23332446, "num_turns": 3, "usage": {"input_tokens": 70560, "output_tokens": 916, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 71476}, "denied_actions": [{"action": "command", "display_name": "RunCommand"}]}}} diff --git a/plugins/antigravity-provider/test/fixtures/tools-plan.capture.ndjson b/plugins/antigravity-provider/test/fixtures/tools-plan.capture.ndjson new file mode 100644 index 0000000000..4a1151d083 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/tools-plan.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", "", "--disable-slash-commands", "--mode", "plan"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 16560, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/tools-plan.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/tools-plan.stderr.ndjson new file mode 100644 index 0000000000..be57aac639 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/tools-plan.stderr.ndjson @@ -0,0 +1,4 @@ +{"t_ms": 292, "stream": "stderr", "data": "warning: --mode plan has no effect while slash command expansion is disabled."} +{"t_ms": 6705, "stream": "stderr", "data": "warning: --mode plan has no effect while slash command expansion is disabled."} +{"t_ms": 13456, "stream": "stderr", "data": "warning: --mode plan has no effect while slash command expansion is disabled."} +{"t_ms": 16400, "stream": "stderr", "data": "jetski: no output produced — a tool required the \"command\" permission that headless mode cannot prompt for, so it was auto-denied. Add an allow-rule under permissions.allow in settings.json (e.g. command()). Alternatively, re-run with --dangerously-skip-permissions to auto-approve all tools."} diff --git a/plugins/antigravity-provider/test/fixtures/tools-plan.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/tools-plan.stdin.ndjson new file mode 100644 index 0000000000..c4d09c8ae7 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/tools-plan.stdin.ndjson @@ -0,0 +1,3 @@ +{"t_ms": 292, "stream": "stdin", "data": {"event": "user", "message": {"role": "user", "content": "Use view_file to read /input.txt. Report its contents. Do not use a shell."}}} +{"t_ms": 6705, "stream": "stdin", "data": {"event": "user", "message": {"role": "user", "content": "Use write_to_file to create /plan.txt with PHASE0_WRITE. Then use replace_file_content to change PHASE0_WRITE to PHASE0_EDIT. No shell. Do not ask me questions."}}} +{"t_ms": 13455, "stream": "stdin", "data": {"event": "user", "message": {"role": "user", "content": "Use run_command exactly once to run printf PHASE0_SHELL in . Report the output. Do not ask me questions."}}} diff --git a/plugins/antigravity-provider/test/fixtures/tools-plan.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/tools-plan.stdout.ndjson new file mode 100644 index 0000000000..473540451c --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/tools-plan.stdout.ndjson @@ -0,0 +1,36 @@ +{"t_ms": 292, "stream": "stdout", "data": {"event": "init", "conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "request-review"}}} +{"t_ms": 319, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 3576, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 1, "state": "DONE", "step_type": "agent_response", "duration_seconds": 3.249717435, "usage": {"input_tokens": 11253, "output_tokens": 339, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11592}}}} +{"t_ms": 3576, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 2, "state": "ACTIVE", "step_type": "tool", "tool_name": "view_file", "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/input.txt"}}}}} +{"t_ms": 3576, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 2, "state": "DONE", "step_type": "tool", "tool_name": "view_file", "duration_seconds": 0.002425685, "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/input.txt"}, "output": "2 lines, 19 bytes"}}}} +{"t_ms": 6197, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "The contents"}}} +{"t_ms": 6324, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " of [`/input.tx"}}} +{"t_ms": 6349, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "t`](file:///"}}} +{"t_ms": 6374, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "tmp/"}}} +{"t_ms": 6451, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "agy-p0/workspace/input.txt) are:\n\n``"}}} +{"t_ms": 6476, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "`\nPHASE0_REA"}}} +{"t_ms": 6502, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "D_MARKER\n```"}}} +{"t_ms": 6705, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 3, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 3.109263873, "usage": {"input_tokens": 11475, "output_tokens": 48, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11523}}}} +{"t_ms": 6705, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "status": "SUCCESS", "response": "The contents of [`/input.txt`](file:///input.txt) are:\n\n```\nPHASE0_READ_MARKER\n```\n", "duration_seconds": 6.388206511, "num_turns": 1, "usage": {"input_tokens": 22728, "output_tokens": 387, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 23115}}}} +{"t_ms": 6732, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 4, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 9502, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 5, "state": "DONE", "step_type": "agent_response", "duration_seconds": 2.757462001, "usage": {"input_tokens": 11642, "output_tokens": 166, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11808}}}} +{"t_ms": 9502, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 6, "state": "ACTIVE", "step_type": "tool", "tool_name": "write_to_file", "tool_info": {"name": "write_to_file", "parameters": {"TargetFile": "/plan.txt"}}}}} +{"t_ms": 9502, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 6, "state": "DONE", "step_type": "tool", "tool_name": "write_to_file", "duration_seconds": 0.003883221, "tool_info": {"name": "write_to_file", "parameters": {"TargetFile": "/plan.txt"}}}}} +{"t_ms": 11716, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 7, "state": "DONE", "step_type": "agent_response", "duration_seconds": 2.227570207, "usage": {"input_tokens": 11811, "output_tokens": 118, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11929}}}} +{"t_ms": 11716, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 8, "state": "ACTIVE", "step_type": "tool", "tool_name": "replace_file_content", "tool_info": {"name": "replace_file_content", "parameters": {"TargetFile": "/plan.txt"}}}}} +{"t_ms": 11742, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 8, "state": "DONE", "step_type": "tool", "tool_name": "replace_file_content", "duration_seconds": 0.002974983, "tool_info": {"name": "replace_file_content", "parameters": {"TargetFile": "/plan.txt"}}}}} +{"t_ms": 13151, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "[`/"}}} +{"t_ms": 13176, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "plan.txt`](file:///tmp/"}}} +{"t_ms": 13252, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "agy-p0/workspace/plan.tx"}}} +{"t_ms": 13278, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "t) was created with `PHA"}}} +{"t_ms": 13303, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "SE0_WRITE` and then upda"}}} +{"t_ms": 13329, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ted to `PHASE0_EDIT` usi"}}} +{"t_ms": 13354, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ng `replace_file_content"}}} +{"t_ms": 13379, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "`."}}} +{"t_ms": 13455, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 9, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 1.716582968, "usage": {"input_tokens": 12103, "output_tokens": 59, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12162}}}} +{"t_ms": 13455, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "status": "SUCCESS", "response": "[`/plan.txt`](file:///plan.txt) was created with `PHASE0_WRITE` and then updated to `PHASE0_EDIT` using `replace_file_content`.\n", "duration_seconds": 13.142130621, "num_turns": 2, "usage": {"input_tokens": 58284, "output_tokens": 730, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 59014}}}} +{"t_ms": 13482, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 10, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 16400, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 11, "state": "DONE", "step_type": "agent_response", "duration_seconds": 2.913339623, "usage": {"input_tokens": 12261, "output_tokens": 74, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12335}}}} +{"t_ms": 16400, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 12, "state": "ACTIVE", "step_type": "tool", "tool_name": "run_command", "tool_info": {"name": "run_command", "parameters": {"CommandLine": "printf PHASE0_SHELL"}}}}} +{"t_ms": 16400, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 12, "state": "ERROR", "step_type": "tool", "tool_name": "run_command", "duration_seconds": 0.002793073, "tool_info": {"name": "run_command", "parameters": {"CommandLine": "printf PHASE0_SHELL"}, "error": {"type": "TOOL_ERROR", "message": "permission check failed for command \"printf PHASE0_SHELL\": user denied permission to run command:\nprintf PHASE0_SHELL\nDo not attempt to circumvent this denial by rephrasing the command, using alternative tools/scripts (e.g. python, sh, curl), or accessing the same target resource. Proceed without performing this action."}}}}} +{"t_ms": 16400, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "status": "SUCCESS", "response": "", "duration_seconds": 16.097866635, "num_turns": 3, "usage": {"input_tokens": 70545, "output_tokens": 804, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 71349}, "denied_actions": [{"action": "command", "display_name": "RunCommand"}]}}} diff --git a/plugins/antigravity-provider/test/fixtures/tools-sandbox.capture.ndjson b/plugins/antigravity-provider/test/fixtures/tools-sandbox.capture.ndjson new file mode 100644 index 0000000000..b845d3e735 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/tools-sandbox.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", "", "--disable-slash-commands", "--sandbox"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 13022, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/tools-sandbox.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/tools-sandbox.stderr.ndjson new file mode 100644 index 0000000000..624a99a74c --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/tools-sandbox.stderr.ndjson @@ -0,0 +1 @@ +{"t_ms": 12723, "stream": "stderr", "data": "jetski: no output produced — a tool required the \"command\" permission that headless mode cannot prompt for, so it was auto-denied. Add an allow-rule under permissions.allow in settings.json (e.g. command()). Alternatively, re-run with --dangerously-skip-permissions to auto-approve all tools."} diff --git a/plugins/antigravity-provider/test/fixtures/tools-sandbox.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/tools-sandbox.stdin.ndjson new file mode 100644 index 0000000000..9ebac338e8 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/tools-sandbox.stdin.ndjson @@ -0,0 +1,3 @@ +{"t_ms": 320, "stream": "stdin", "data": {"event": "user", "message": {"role": "user", "content": "Use view_file to read /input.txt. Report its contents. Do not use a shell."}}} +{"t_ms": 4959, "stream": "stdin", "data": {"event": "user", "message": {"role": "user", "content": "Use write_to_file to create /sandbox.txt with PHASE0_WRITE. Then use replace_file_content to change PHASE0_WRITE to PHASE0_EDIT. No shell. Do not ask me questions."}}} +{"t_ms": 10581, "stream": "stdin", "data": {"event": "user", "message": {"role": "user", "content": "Use run_command exactly once to run printf PHASE0_SHELL in . Report the output. Do not ask me questions."}}} diff --git a/plugins/antigravity-provider/test/fixtures/tools-sandbox.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/tools-sandbox.stdout.ndjson new file mode 100644 index 0000000000..6d63907468 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/tools-sandbox.stdout.ndjson @@ -0,0 +1,40 @@ +{"t_ms": 319, "stream": "stdout", "data": {"event": "init", "conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "request-review"}}} +{"t_ms": 346, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 3169, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 1, "state": "DONE", "step_type": "agent_response", "duration_seconds": 2.806171145, "usage": {"input_tokens": 12031, "output_tokens": 307, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12338}}}} +{"t_ms": 3169, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 2, "state": "ACTIVE", "step_type": "tool", "tool_name": "view_file", "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/input.txt"}}}}} +{"t_ms": 3169, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 2, "state": "DONE", "step_type": "tool", "tool_name": "view_file", "duration_seconds": 0.002973019, "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/input.txt"}, "output": "2 lines, 19 bytes"}}}} +{"t_ms": 4705, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "The contents"}}} +{"t_ms": 4755, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " of [input.txt](file:///"}}} +{"t_ms": 4781, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "tmp/agy-p0/w"}}} +{"t_ms": 4806, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "orkspace/input"}}} +{"t_ms": 4857, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": ".txt) are:\n\n"}}} +{"t_ms": 4882, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "```text\nPHASE0_READ_MARK"}}} +{"t_ms": 4908, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ER\n```"}}} +{"t_ms": 4959, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 3, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 1.791180207, "usage": {"input_tokens": 12253, "output_tokens": 39, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12292}}}} +{"t_ms": 4959, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "status": "SUCCESS", "response": "The contents of [input.txt](file:///input.txt) are:\n\n```text\nPHASE0_READ_MARKER\n```\n", "duration_seconds": 4.624371092, "num_turns": 1, "usage": {"input_tokens": 24284, "output_tokens": 346, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 24630}}}} +{"t_ms": 4986, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 4, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 6674, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 5, "state": "DONE", "step_type": "agent_response", "duration_seconds": 1.678518989, "usage": {"input_tokens": 12411, "output_tokens": 78, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12489}}}} +{"t_ms": 6674, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 6, "state": "ACTIVE", "step_type": "tool", "tool_name": "write_to_file", "tool_info": {"name": "write_to_file", "parameters": {"TargetFile": "/sandbox.txt"}}}}} +{"t_ms": 6674, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 6, "state": "DONE", "step_type": "tool", "tool_name": "write_to_file", "duration_seconds": 0.002765401, "tool_info": {"name": "write_to_file", "parameters": {"TargetFile": "/sandbox.txt"}}}}} +{"t_ms": 8939, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 7, "state": "DONE", "step_type": "agent_response", "duration_seconds": 2.278065329, "usage": {"input_tokens": 12577, "output_tokens": 118, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12695}}}} +{"t_ms": 8939, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 8, "state": "ACTIVE", "step_type": "tool", "tool_name": "replace_file_content", "tool_info": {"name": "replace_file_content", "parameters": {"TargetFile": "/sandbox.txt"}}}}} +{"t_ms": 8965, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 8, "state": "DONE", "step_type": "tool", "tool_name": "replace_file_content", "duration_seconds": 0.002992846, "tool_info": {"name": "replace_file_content", "parameters": {"TargetFile": "/sandbox.txt"}}}}} +{"t_ms": 10124, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "### Summary"}}} +{"t_ms": 10251, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " of Changes\n\n1. **Created File**: Us"}}} +{"t_ms": 10276, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ed `write_to"}}} +{"t_ms": 10302, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "_file` to create"}}} +{"t_ms": 10378, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " [sandbox.tx"}}} +{"t_ms": 10403, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "t](file:///wo"}}} +{"t_ms": 10428, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "rkspace/sandbox.txt) wit"}}} +{"t_ms": 10454, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "h the conten"}}} +{"t_ms": 10479, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "t `PHASE0_WRITE`.\n2. **Updated Con"}}} +{"t_ms": 10504, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "tent**: Used `replace_fi"}}} +{"t_ms": 10530, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "le_content` to replace `"}}} +{"t_ms": 10555, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "PHASE0_WRITE` with `PHAS"}}} +{"t_ms": 10581, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 9, "state": "DONE", "step_type": "agent_response", "text_delta": "E0_EDIT`.\n", "duration_seconds": 1.630993868, "usage": {"input_tokens": 12869, "output_tokens": 83, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12952}}}} +{"t_ms": 10581, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "status": "SUCCESS", "response": "### Summary of Changes\n\n1. **Created File**: Used `write_to_file` to create [sandbox.txt](file:///sandbox.txt) with the content `PHASE0_WRITE`.\n2. **Updated Content**: Used `replace_file_content` to replace `PHASE0_WRITE` with `PHASE0_EDIT`.\n", "duration_seconds": 10.25241531, "num_turns": 2, "usage": {"input_tokens": 62141, "output_tokens": 625, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 62766}}}} +{"t_ms": 10608, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 10, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 12723, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 11, "state": "DONE", "step_type": "agent_response", "duration_seconds": 2.117119481, "usage": {"input_tokens": 13051, "output_tokens": 80, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 13131}}}} +{"t_ms": 12723, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 12, "state": "ACTIVE", "step_type": "tool", "tool_name": "run_command", "tool_info": {"name": "run_command", "parameters": {"CommandLine": "printf PHASE0_SHELL"}}}}} +{"t_ms": 12723, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 12, "state": "ERROR", "step_type": "tool", "tool_name": "run_command", "duration_seconds": 0.00280781, "tool_info": {"name": "run_command", "parameters": {"CommandLine": "printf PHASE0_SHELL"}, "error": {"type": "TOOL_ERROR", "message": "permission check failed for command \"printf PHASE0_SHELL\": user denied permission to run command:\nprintf PHASE0_SHELL\nDo not attempt to circumvent this denial by rephrasing the command, using alternative tools/scripts (e.g. python, sh, curl), or accessing the same target resource. Proceed without performing this action."}}}}} +{"t_ms": 12723, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "status": "SUCCESS", "response": "", "duration_seconds": 12.395964579, "num_turns": 3, "usage": {"input_tokens": 75192, "output_tokens": 705, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 75897}, "denied_actions": [{"action": "command", "display_name": "RunCommand"}]}}} diff --git a/plugins/antigravity-provider/test/fixtures/tools-skip-permissions.capture.ndjson b/plugins/antigravity-provider/test/fixtures/tools-skip-permissions.capture.ndjson new file mode 100644 index 0000000000..dd49b19784 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/tools-skip-permissions.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", "", "--disable-slash-commands", "--dangerously-skip-permissions"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 17771, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/tools-skip-permissions.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/tools-skip-permissions.stderr.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/tools-skip-permissions.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/tools-skip-permissions.stdin.ndjson new file mode 100644 index 0000000000..f91311bcf7 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/tools-skip-permissions.stdin.ndjson @@ -0,0 +1,3 @@ +{"t_ms": 292, "stream": "stdin", "data": {"event": "user", "message": {"role": "user", "content": "Use view_file to read /input.txt. Report its contents. Do not use a shell."}}} +{"t_ms": 5051, "stream": "stdin", "data": {"event": "user", "message": {"role": "user", "content": "Use write_to_file to create /skip-permissions.txt with PHASE0_WRITE. Then use replace_file_content to change PHASE0_WRITE to PHASE0_EDIT. No shell. Do not ask me questions."}}} +{"t_ms": 11704, "stream": "stdin", "data": {"event": "user", "message": {"role": "user", "content": "Use run_command exactly once to run printf PHASE0_SHELL in . Report the output. Do not ask me questions."}}} diff --git a/plugins/antigravity-provider/test/fixtures/tools-skip-permissions.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/tools-skip-permissions.stdout.ndjson new file mode 100644 index 0000000000..9d2f1fbafa --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/tools-skip-permissions.stdout.ndjson @@ -0,0 +1,42 @@ +{"t_ms": 291, "stream": "stdout", "data": {"event": "init", "conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "always-proceed"}}} +{"t_ms": 318, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 2658, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 1, "state": "DONE", "step_type": "agent_response", "duration_seconds": 2.317552401, "usage": {"input_tokens": 11263, "output_tokens": 150, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11413}}}} +{"t_ms": 2658, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 2, "state": "ACTIVE", "step_type": "tool", "tool_name": "view_file", "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/input.txt"}}}}} +{"t_ms": 2658, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 2, "state": "DONE", "step_type": "tool", "tool_name": "view_file", "duration_seconds": 0.002646148, "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/input.txt"}, "output": "2 lines, 19 bytes"}}}} +{"t_ms": 4645, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "The contents"}}} +{"t_ms": 4721, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " of [input.txt](file:///"}}} +{"t_ms": 4746, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "tmp/agy-p0/workspace/inp"}}} +{"t_ms": 4772, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ut"}}} +{"t_ms": 4848, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": ".txt) are:\n\n```\nPHASE0_READ_MARKER\n`"}}} +{"t_ms": 4873, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "``"}}} +{"t_ms": 5025, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 3, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 2.386909321, "usage": {"input_tokens": 11484, "output_tokens": 38, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11522}}}} +{"t_ms": 5051, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "status": "SUCCESS", "response": "The contents of [input.txt](file:///input.txt) are:\n\n```\nPHASE0_READ_MARKER\n```\n", "duration_seconds": 4.732286256, "num_turns": 1, "usage": {"input_tokens": 22747, "output_tokens": 188, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 22935}}}} +{"t_ms": 5078, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 4, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 7346, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 5, "state": "DONE", "step_type": "agent_response", "duration_seconds": 2.260780071, "usage": {"input_tokens": 11643, "output_tokens": 152, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11795}}}} +{"t_ms": 7346, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 6, "state": "ACTIVE", "step_type": "tool", "tool_name": "write_to_file", "tool_info": {"name": "write_to_file", "parameters": {"TargetFile": "/skip-permissions.txt"}}}}} +{"t_ms": 7346, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 6, "state": "DONE", "step_type": "tool", "tool_name": "write_to_file", "duration_seconds": 0.003788063, "tool_info": {"name": "write_to_file", "parameters": {"TargetFile": "/skip-permissions.txt"}}}}} +{"t_ms": 10113, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 7, "state": "DONE", "step_type": "agent_response", "duration_seconds": 2.769162417, "usage": {"input_tokens": 11812, "output_tokens": 117, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11929}}}} +{"t_ms": 10113, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 8, "state": "ACTIVE", "step_type": "tool", "tool_name": "replace_file_content", "tool_info": {"name": "replace_file_content", "parameters": {"TargetFile": "/skip-permissions.txt"}}}}} +{"t_ms": 10113, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 8, "state": "DONE", "step_type": "tool", "tool_name": "replace_file_content", "duration_seconds": 0.004067275, "tool_info": {"name": "replace_file_content", "parameters": {"TargetFile": "/skip-permissions.txt"}}}}} +{"t_ms": 11349, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "I have"}}} +{"t_ms": 11450, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " created [skip-permissio"}}} +{"t_ms": 11476, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ns.txt](file"}}} +{"t_ms": 11501, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "://"}}} +{"t_ms": 11526, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "/skip"}}} +{"t_ms": 11552, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "-permissions.txt) with `"}}} +{"t_ms": 11577, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "PHASE0_WRITE` and then u"}}} +{"t_ms": 11603, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "pdated its c"}}} +{"t_ms": 11628, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ontent to `PHASE0_EDIT` using `repla"}}} +{"t_ms": 11653, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ce_file_cont"}}} +{"t_ms": 11678, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ent`."}}} +{"t_ms": 11704, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 9, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 1.563629905, "usage": {"input_tokens": 12105, "output_tokens": 56, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12161}}}} +{"t_ms": 11704, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "status": "SUCCESS", "response": "I have created [skip-permissions.txt](file:///skip-permissions.txt) with `PHASE0_WRITE` and then updated its content to `PHASE0_EDIT` using `replace_file_content`.\n", "duration_seconds": 11.386053268, "num_turns": 2, "usage": {"input_tokens": 58307, "output_tokens": 513, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 58820}}}} +{"t_ms": 11731, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 10, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 16142, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 11, "state": "DONE", "step_type": "agent_response", "duration_seconds": 4.411191659, "usage": {"input_tokens": 12260, "output_tokens": 390, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12650}}}} +{"t_ms": 16143, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 12, "state": "ACTIVE", "step_type": "tool", "tool_name": "run_command", "tool_info": {"name": "run_command", "parameters": {"CommandLine": "printf PHASE0_SHELL"}}}}} +{"t_ms": 16143, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 12, "state": "DONE", "step_type": "tool", "tool_name": "run_command", "duration_seconds": 0.007492259, "tool_info": {"name": "run_command", "parameters": {"CommandLine": "printf PHASE0_SHELL"}, "output": "PHASE0_SHELL"}}}} +{"t_ms": 17501, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 13, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "The command"}}} +{"t_ms": 17578, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 13, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " output is:\n"}}} +{"t_ms": 17603, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 13, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "\n```\nPHASE0_SHELL\n```"}}} +{"t_ms": 17680, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 13, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 1.518696431, "usage": {"input_tokens": 12396, "output_tokens": 16, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12412}}}} +{"t_ms": 17680, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "status": "SUCCESS", "response": "The command output is:\n\n```\nPHASE0_SHELL\n```\n", "duration_seconds": 17.367491474, "num_turns": 3, "usage": {"input_tokens": 82963, "output_tokens": 919, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 83882}}}} diff --git a/plugins/antigravity-provider/test/fixtures/version.capture.ndjson b/plugins/antigravity-provider/test/fixtures/version.capture.ndjson new file mode 100644 index 0000000000..ac8e605e77 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/version.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--version"], "cwd": "", "home": "", "settings": null, "env": {}}} +{"t_ms": 284, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/version.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/version.stderr.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/version.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/version.stdin.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/version.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/version.stdout.ndjson new file mode 100644 index 0000000000..ce8e23e783 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/version.stdout.ndjson @@ -0,0 +1 @@ +{"t_ms": 269, "stream": "stdout", "data": "1.2.13"} diff --git a/public-docs/supported-providers.md b/public-docs/supported-providers.md index f8edad9953..588fd96195 100644 --- a/public-docs/supported-providers.md +++ b/public-docs/supported-providers.md @@ -18,6 +18,37 @@ Work out of the box once the underlying CLI is installed and authenticated. - [Codex](/docs/codex). OpenAI's workspace agent with sandbox controls and optional network access. - [OpenCode](https://opencode.ai/). Open-source coding assistant with multi-provider model support. - [Pi](https://pi.dev). Minimal terminal-based coding agent with multi-provider LLM support. +- [Antigravity](#antigravity). Uses your installed `agy` CLI through a built-in provider plugin. + +### Antigravity + +Install the Antigravity CLI (`agy`) on your daemon host, then run `agy` once to sign in. Pick +**Antigravity** in Paseo. The provider ships with Paseo; no separate plugin install is required. + +**Default**, **Accept edits**, and **Plan** allow workspace edits and deny shell commands. +**Full access** allows everything. Plan does not enforce read-only access. To run a denied shell +command, choose Full access and send another prompt. + +Paseo MCP tools and steering are unavailable. Your own `agy` MCP servers still work. Attached +images are passed as file references for the agent to read. + +If `agy` is outside your daemon's PATH, override its command in `~/.paseo/config.json`. Use `env` +for provider-specific environment variables: + +```json +{ + "agents": { + "providers": { + "antigravity": { + "command": ["/absolute/path/to/agy"], + "env": { "EXAMPLE_VARIABLE": "value" } + } + } + } +} +``` + +See [Custom providers](/docs/custom-providers) for command and environment overrides. ## ACP catalog From 8332f4d1ff921951ed2725b15a2108d8afdf2182 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Wed, 30 Sep 2026 14:56:41 +0100 Subject: [PATCH 023/100] feat(muse): add Muse Code as a built-in provider plugin (#5719) * test(muse): record real host protocol fixtures * feat(muse): add built-in MSP provider Co-authored-by: millerben95 * fix(muse): expose provider settings in the app The daemon registered host settings without a client contribution, leaving sandbox, network, and workspace trust unreachable. Share the definition with a settings screen and expose it through Command Center even when installed plugins are disabled. * docs(muse): publish setup and provider support * test: correct runtime fixtures and restart readiness * fix(muse): hide reminder housekeeping from the timeline * fix(muse): use the official Muse Code mark Source: https://muse.ai/images/favicon/app-squiggle.svg Preserve the official silhouette path verbatim. Remove its gradient definition and use currentColor, scaling the original 100-unit coordinates into the 24-unit viewBox. Black silhouette renders match pixel-for-pixel. * fix(muse): adapt to current plugin APIs Use the screen API introduced with sidebar plugin items. Usage reports now come from source account discovery; remove the retired agent usage-reference request and headline marker, and exercise discovery through the existing provider fixtures. --------- Co-authored-by: millerben95 --- CLAUDE.md | 2 +- README.md | 6 +- docs/plugins.md | 23 +- docs/providers.md | 10 +- .../25-daemon-restart-supervisor.test.ts | 8 +- .../server/daemon-e2e/muse.real.e2e.test.ts | 95 + .../src/server/plugins/builtin/index.ts | 1 + .../website/src/components/agent-icons.tsx | 34 + .../website/src/components/landing-page.tsx | 18 +- .../website/src/components/site-footer.tsx | 12 + .../content/alternatives/claude-desktop.md | 42 +- .../src/content/alternatives/codex-app.md | 2 +- .../src/content/alternatives/conductor.md | 34 +- .../src/content/alternatives/happy-coder.md | 34 +- .../src/content/alternatives/openchamber.md | 36 +- .../content/alternatives/opencode-desktop.md | 38 +- .../website/src/content/alternatives/orca.md | 40 +- .../src/content/alternatives/superset.md | 2 +- packages/website/src/data/agent-pages.ts | 20 + packages/website/src/llms.ts | 2 +- packages/website/src/routeTree.gen.ts | 42 + packages/website/src/routes/agents.tsx | 2 +- packages/website/src/routes/antigravity.tsx | 4 + packages/website/src/routes/muse-code.tsx | 4 + plugins/muse-provider/client/settings.tsx | 103 + plugins/muse-provider/icon.svg | 1 + plugins/muse-provider/index.client.tsx | 20 + plugins/muse-provider/index.server.ts | 18 + plugins/muse-provider/paseo-plugin.json | 1 + plugins/muse-provider/server/catalog.ts | 91 + plugins/muse-provider/server/children.ts | 94 + plugins/muse-provider/server/commands.ts | 56 + plugins/muse-provider/server/connection.ts | 193 ++ plugins/muse-provider/server/errors.ts | 81 + plugins/muse-provider/server/msp.d.ts | 2690 +++++++++++++++++ plugins/muse-provider/server/provider.ts | 223 ++ plugins/muse-provider/server/questions.ts | 86 + plugins/muse-provider/server/recovery.ts | 45 + plugins/muse-provider/server/session.ts | 635 ++++ plugins/muse-provider/server/sessions.ts | 57 + plugins/muse-provider/server/settings.ts | 17 + plugins/muse-provider/server/timeline.ts | 234 ++ plugins/muse-provider/server/usage.ts | 111 + plugins/muse-provider/server/wire.ts | 278 ++ plugins/muse-provider/shared/settings.ts | 24 + plugins/muse-provider/test/fake-muse.mjs | 404 +++ plugins/muse-provider/test/fixtures/README.md | 78 + .../fixtures/auto-review-unavailable.ndjson | 5 + .../test/fixtures/catalog-controls.ndjson | 26 + .../test/fixtures/image-followup.ndjson | 51 + .../test/fixtures/initialize-history.ndjson | 3 + .../test/fixtures/interrupt.ndjson | 28 + .../test/fixtures/mcp-stdio.ndjson | 97 + .../test/fixtures/multi-stage-approval.ndjson | 51 + .../test/fixtures/phase3-cancel.ndjson | 31 + .../test/fixtures/phase3-child-read.ndjson | 7 + .../fixtures/phase3-compact-history.ndjson | 13 + .../test/fixtures/phase3-compact-limit.ndjson | 15 + .../test/fixtures/phase3-compact.ndjson | 11 + .../test/fixtures/phase3-controls.ndjson | 51 + .../fixtures/phase3-default-workflow.ndjson | 7 + .../test/fixtures/phase3-project-skill.ndjson | 34 + .../test/fixtures/phase3-user-skill.ndjson | 32 + .../test/fixtures/resume-reasoning.ndjson | 27 + .../test/fixtures/resume-source.ndjson | 26 + .../test/fixtures/resume-with-cursor.ndjson | 40 + .../fixtures/resume-without-cursor.ndjson | 28 + .../muse-provider/test/fixtures/skill.ndjson | 31 + .../test/fixtures/staged-approval.ndjson | 51 + .../muse-provider/test/fixtures/steer.ndjson | 48 + .../test/fixtures/subagent.ndjson | 39 + .../test/fixtures/text-reasoning.ndjson | 53 + .../test/fixtures/tools-edit.ndjson | 48 + .../test/fixtures/unsafe-path.ndjson | 5 + plugins/muse-provider/test/provider.test.ts | 1268 ++++++++ plugins/tsconfig.json | 3 +- public-docs/custom-providers.md | 4 +- public-docs/docker.md | 2 +- public-docs/muse-code.md | 58 + public-docs/providers.md | 4 +- public-docs/security.md | 2 +- public-docs/supported-providers.md | 1 + scripts/builtin-plugins-dist.test.mjs | 7 +- 83 files changed, 8116 insertions(+), 142 deletions(-) create mode 100644 packages/server/src/server/daemon-e2e/muse.real.e2e.test.ts create mode 100644 packages/website/src/routes/antigravity.tsx create mode 100644 packages/website/src/routes/muse-code.tsx create mode 100644 plugins/muse-provider/client/settings.tsx create mode 100644 plugins/muse-provider/icon.svg create mode 100644 plugins/muse-provider/index.client.tsx create mode 100644 plugins/muse-provider/index.server.ts create mode 100644 plugins/muse-provider/paseo-plugin.json create mode 100644 plugins/muse-provider/server/catalog.ts create mode 100644 plugins/muse-provider/server/children.ts create mode 100644 plugins/muse-provider/server/commands.ts create mode 100644 plugins/muse-provider/server/connection.ts create mode 100644 plugins/muse-provider/server/errors.ts create mode 100644 plugins/muse-provider/server/msp.d.ts create mode 100644 plugins/muse-provider/server/provider.ts create mode 100644 plugins/muse-provider/server/questions.ts create mode 100644 plugins/muse-provider/server/recovery.ts create mode 100644 plugins/muse-provider/server/session.ts create mode 100644 plugins/muse-provider/server/sessions.ts create mode 100644 plugins/muse-provider/server/settings.ts create mode 100644 plugins/muse-provider/server/timeline.ts create mode 100644 plugins/muse-provider/server/usage.ts create mode 100644 plugins/muse-provider/server/wire.ts create mode 100644 plugins/muse-provider/shared/settings.ts create mode 100644 plugins/muse-provider/test/fake-muse.mjs create mode 100644 plugins/muse-provider/test/fixtures/README.md create mode 100644 plugins/muse-provider/test/fixtures/auto-review-unavailable.ndjson create mode 100644 plugins/muse-provider/test/fixtures/catalog-controls.ndjson create mode 100644 plugins/muse-provider/test/fixtures/image-followup.ndjson create mode 100644 plugins/muse-provider/test/fixtures/initialize-history.ndjson create mode 100644 plugins/muse-provider/test/fixtures/interrupt.ndjson create mode 100644 plugins/muse-provider/test/fixtures/mcp-stdio.ndjson create mode 100644 plugins/muse-provider/test/fixtures/multi-stage-approval.ndjson create mode 100644 plugins/muse-provider/test/fixtures/phase3-cancel.ndjson create mode 100644 plugins/muse-provider/test/fixtures/phase3-child-read.ndjson create mode 100644 plugins/muse-provider/test/fixtures/phase3-compact-history.ndjson create mode 100644 plugins/muse-provider/test/fixtures/phase3-compact-limit.ndjson create mode 100644 plugins/muse-provider/test/fixtures/phase3-compact.ndjson create mode 100644 plugins/muse-provider/test/fixtures/phase3-controls.ndjson create mode 100644 plugins/muse-provider/test/fixtures/phase3-default-workflow.ndjson create mode 100644 plugins/muse-provider/test/fixtures/phase3-project-skill.ndjson create mode 100644 plugins/muse-provider/test/fixtures/phase3-user-skill.ndjson create mode 100644 plugins/muse-provider/test/fixtures/resume-reasoning.ndjson create mode 100644 plugins/muse-provider/test/fixtures/resume-source.ndjson create mode 100644 plugins/muse-provider/test/fixtures/resume-with-cursor.ndjson create mode 100644 plugins/muse-provider/test/fixtures/resume-without-cursor.ndjson create mode 100644 plugins/muse-provider/test/fixtures/skill.ndjson create mode 100644 plugins/muse-provider/test/fixtures/staged-approval.ndjson create mode 100644 plugins/muse-provider/test/fixtures/steer.ndjson create mode 100644 plugins/muse-provider/test/fixtures/subagent.ndjson create mode 100644 plugins/muse-provider/test/fixtures/text-reasoning.ndjson create mode 100644 plugins/muse-provider/test/fixtures/tools-edit.ndjson create mode 100644 plugins/muse-provider/test/fixtures/unsafe-path.ndjson create mode 100644 plugins/muse-provider/test/provider.test.ts create mode 100644 public-docs/muse-code.md diff --git a/CLAUDE.md b/CLAUDE.md index b1f8a294ed..e1ac653338 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,7 +2,7 @@ Paseo is a mobile app for monitoring and controlling your local AI coding agents from anywhere. Your dev environment, in your pocket. Connects directly to your actual development environment — your code stays on your machine. -**Supported agents:** Claude Code, Codex, GitHub Copilot, OpenCode, and Pi. +**Supported agents:** Claude Code, Codex, GitHub Copilot, OpenCode, Pi, Antigravity, and Muse Code. ## Repository map diff --git a/README.md b/README.md index 120672746b..aa0a841e7e 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@

-

One interface for Claude Code, Codex, Copilot, OpenCode, and Pi agents.

+

One interface for Claude Code, Codex, Copilot, OpenCode, Pi, Antigravity, and Muse Code agents.

Paseo app screenshot @@ -42,7 +42,7 @@ Run agents in parallel on your own machines. Ship from your phone or your desk. - **Self-hosted:** Agents run on your machine with your full dev environment. Use your tools, your configs, and your skills. -- **Multi-provider:** Claude Code, Codex, Copilot, OpenCode, and Pi through the same interface. Pick the right model for each job. +- **Multi-provider:** Claude Code, Codex, Copilot, OpenCode, Pi, Antigravity, and Muse Code through the same interface. Pick the right model for each job. - **Voice control:** Dictate tasks or talk through problems in voice mode. Hands-free when you need it. - **Cross-device:** iOS, Android, desktop, web, and CLI. Start work at your desk, check in from your phone, script it from the terminal. - **Privacy-first:** Paseo doesn't have any telemetry, tracking, or forced log-ins. @@ -68,6 +68,8 @@ You need at least one agent CLI installed and configured with your credentials: - [GitHub Copilot](https://github.com/features/copilot/cli/) - [OpenCode](https://github.com/anomalyco/opencode) - [Pi](https://pi.dev) +- [Antigravity](https://paseo.sh/docs/supported-providers#antigravity) +- [Muse Code](https://paseo.sh/docs/muse-code) ### Desktop app (recommended) diff --git a/docs/plugins.md b/docs/plugins.md index f3c0572689..a354a20374 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -88,12 +88,23 @@ never deletes it. The global `pluginsEnabled` switch remains available. ## Built-in plugins -Built-in plugins live in `plugins//` and ship with the daemon. Add a directory and one ID to -`builtinPlugins` in `packages/server/src/server/plugins/builtin/index.ts`. The workspace, build -copy, and CI checks cover every listed directory; unlisted directories do not load. Built-ins run -in process, ignore `pluginsEnabled`, and do not appear in `config.json` or the installed plugin -list. Their client bundles appear in the plugin catalog. Editing one in development requires a -daemon restart. Directory, Git, and npm installs cannot use a built-in ID. +Built-in plugins ship from `plugins//`, with `paseo-plugin.json`, `index.server.ts`, +`server/`, and optional client entry and icon. Add the plugin ID to `builtinPlugins` in +`packages/server/src/server/plugins/builtin/index.ts`; the workspace, build copy, and CI +checks cover that registry. Unlisted directories do not load. + +Built-ins run in process and remain active independently of `pluginsEnabled`. They are +absent from the installed plugin list and source configuration; their client bundles +appear in the plugin catalog. Editing one in development requires a daemon restart. +Directory, Git, and npm installs cannot use a built-in ID. + +Provider plugins use separate installation and provider IDs. `muse-provider` registers the +selectable `muse` provider (Muse Code) and a usage source. Its `status({ launch })` reports +availability and a diagnostic after the daemon resolves the executable. Configure command, +environment, or enablement overrides under `agents.providers.muse`. Omit `extends` to keep +the bundled integration; an entry with `extends` shadows it with a custom provider. See +[provider contributions](#contribute-a-provider) for the contract and +[Muse Code](../public-docs/muse-code.md) for setup, settings, and version limitations. ## Install a Git source diff --git a/docs/providers.md b/docs/providers.md index d44a886674..917c2512d1 100644 --- a/docs/providers.md +++ b/docs/providers.md @@ -6,7 +6,7 @@ server integrations. ## Plugin providers Keep a bundled provider in `plugins//` and register it through -`@getpaseo/plugin/server/provider`. Antigravity follows this pattern. Built-in loading and SDK +`@getpaseo/plugin/server/provider`. Antigravity and Muse Code follow this pattern. Built-in loading and SDK import rules belong to [plugins.md](plugins.md#built-in-plugins); the [public provider guide](../public-docs/plugins/providers.md) covers the provider contract. @@ -15,8 +15,10 @@ executable resolution and applies `agents.providers..command` and ` connecting. Register the provider's icon with the plugin rather than adding it to the app's provider icon map. You do not need a core manifest entry or provider factory. -For Antigravity installation, modes, and limitations, see -[Supported providers](../public-docs/supported-providers.md#antigravity). +| Provider | Transport | Setup and limitations | +| ----------- | ------------------------------------------ | ---------------------------------------------------------------- | +| Antigravity | Installed `agy` CLI | [Antigravity](../public-docs/supported-providers.md#antigravity) | +| Muse Code | MSP over one `muse serve` host per session | [Muse Code](../public-docs/muse-code.md) | ## Provider-native session options @@ -85,7 +87,7 @@ model; it does not override the model list returned by a resolver. Implement the `AgentClient` and `AgentSession` interfaces from `agent-sdk-types.ts` yourself. This gives full control but requires you to handle process management, streaming, permissions, and session persistence from scratch. -Existing direct providers: `claude` (in `providers/claude/agent.ts`), `codex` (`codex-app-server-agent.ts`), `opencode` (`opencode/runtime-client.ts`), `pi` (`providers/pi/agent.ts`), and `omp` (`providers/omp/agent.ts`). The dev-only `mock` provider (`mock-load-test-agent.ts`) is also direct. +Core direct providers: `claude` (in `providers/claude/agent.ts`), `codex` (`codex-app-server-agent.ts`), `opencode` (`opencode/runtime-client.ts`), `pi` (`providers/pi/agent.ts`), and `omp` (`providers/omp/agent.ts`). The dev-only `mock` provider (`mock-load-test-agent.ts`) is also direct. Claude first-party model metadata lives in `packages/server/src/server/agent/providers/claude/model-manifest.ts`. When adding or updating a Claude model, update that manifest only; the model picker thinking options and Claude-specific feature gates are derived from the manifest. Do not add model-specific Claude capability lists in feature code. diff --git a/packages/cli/tests/25-daemon-restart-supervisor.test.ts b/packages/cli/tests/25-daemon-restart-supervisor.test.ts index f38caa5601..d3ae10e19c 100644 --- a/packages/cli/tests/25-daemon-restart-supervisor.test.ts +++ b/packages/cli/tests/25-daemon-restart-supervisor.test.ts @@ -21,6 +21,7 @@ import { runLocalPaseo } from "./helpers/local-cli.ts"; import { getAvailablePort } from "./helpers/network.ts"; const pollIntervalMs = 100; +const daemonReadyTimeoutMs = 120_000; const testEnv = { PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD: process.env.PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD ?? "0", PASEO_DICTATION_ENABLED: process.env.PASEO_DICTATION_ENABLED ?? "0", @@ -135,7 +136,7 @@ import('node:fs').then(({appendFileSync}) => { supervisor = await readDaemonInstance(paseoHome); return supervisor?.pid === supervisorProcess?.pid && Boolean(supervisor?.listen); }, - 120000, + daemonReadyTimeoutMs, "daemon did not publish its bound endpoint in time", ); assert(supervisor?.listen, "owned supervisor should publish its bound endpoint"); @@ -176,7 +177,8 @@ import('node:fs').then(({appendFileSync}) => { "restart request should be acknowledged", ); - const deadline = Date.now() + 20000; + // A restart includes worker startup and client reconnection, as initial readiness does. + const deadline = Date.now() + daemonReadyTimeoutMs; let statusAfterRestart = statusBeforeRestart; await waitFor( async () => { @@ -193,7 +195,7 @@ import('node:fs').then(({appendFileSync}) => { throw error; } }, - 20000, + daemonReadyTimeoutMs, "worker pid did not change after restart request", ); assert.notStrictEqual( diff --git a/packages/server/src/server/daemon-e2e/muse.real.e2e.test.ts b/packages/server/src/server/daemon-e2e/muse.real.e2e.test.ts new file mode 100644 index 0000000000..d9d473511a --- /dev/null +++ b/packages/server/src/server/daemon-e2e/muse.real.e2e.test.ts @@ -0,0 +1,95 @@ +import { execFileSync } from "node:child_process"; +import { mkdtemp, rm, writeFile } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { expect, test } from "vitest"; +import { isCommandAvailable } from "../../executable-resolution/executable-resolution.js"; +import { loadPersistedConfig } from "../persisted-config.js"; +import { resolvePaseoHome } from "../paseo-home.js"; +import { BuiltinPluginLoader } from "../plugins/builtin/index.js"; +import { createTestPaseoDaemon } from "../test-utils/paseo-daemon.js"; +import { DaemonClient } from "../test-utils/daemon-client.js"; + +test("Muse publishes its client settings bundle while installed plugins are disabled", async () => { + const daemon = await createTestPaseoDaemon({ + builtinPlugins: new BuiltinPluginLoader(undefined, ["muse-provider"]), + }); + const client = new DaemonClient({ + url: `ws://127.0.0.1:${daemon.port}/ws`, + appVersion: "0.10.0", + }); + try { + await client.connect(); + const muse = (await client.getPluginCatalog()).find((plugin) => plugin.id === "muse-provider"); + expect(muse?.clientBundle).toBeTypeOf("string"); + expect(muse?.clientBundle).not.toBe(""); + } finally { + await client.close(); + await daemon.close(); + } +}); + +test("Muse creates an agent, streams a text reply, and executes a real tool", async (context) => { + if (!(await isCommandAvailable("muse"))) { + context.skip(); + return; + } + const cwd = await mkdtemp(path.join(os.tmpdir(), "paseo-muse-e2e-")); + await writeFile(path.join(cwd, "f"), "MUSE_TOOL_E2E_OK\n"); + execFileSync("git", ["init", cwd], { stdio: "ignore" }); + const configured = loadPersistedConfig(resolvePaseoHome()).agents?.providers?.muse; + const daemon = await createTestPaseoDaemon({ + builtinPlugins: new BuiltinPluginLoader(undefined, ["muse-provider"]), + providerOverrides: { muse: { ...configured, enabled: true } }, + }); + const client = new DaemonClient({ + url: `ws://127.0.0.1:${daemon.port}/ws`, + appVersion: "0.10.0", + }); + try { + await client.connect(); + await client.fetchAgents({ subscribe: {} }); + const agent = await client.createAgent({ provider: "muse", cwd, modeId: "allowAll" }); + await client.sendMessage(agent.id, "Reply exactly MUSE_TEXT_E2E_OK. Do not use tools."); + expect((await client.waitForFinish(agent.id, 120000)).status).toBe("idle"); + const reply = await client.fetchAgentTimeline(agent.id, { + direction: "tail", + limit: 0, + projection: "canonical", + }); + expect( + reply.entries.some( + (entry) => + entry.item.type === "assistant_message" && entry.item.text.includes("MUSE_TEXT_E2E_OK"), + ), + ).toBe(true); + await client.sendMessage( + agent.id, + "Read the file f with the read_file tool and report its contents.", + ); + expect((await client.waitForFinish(agent.id, 120000)).status).toBe("idle"); + const tools = await client.fetchAgentTimeline(agent.id, { + direction: "tail", + limit: 0, + projection: "canonical", + }); + expect( + tools.entries.some( + (entry) => + entry.item.type === "tool_call" && + entry.item.name === "read_file" && + entry.item.status === "completed", + ), + ).toBe(true); + expect( + tools.entries.some( + (entry) => + entry.item.type === "assistant_message" && entry.item.text.includes("MUSE_TOOL_E2E_OK"), + ), + ).toBe(true); + } finally { + await client.close(); + await daemon.close(); + await rm(cwd, { recursive: true, force: true }); + } +}, 300000); diff --git a/packages/server/src/server/plugins/builtin/index.ts b/packages/server/src/server/plugins/builtin/index.ts index 52f9c9df3c..eb6aab147e 100644 --- a/packages/server/src/server/plugins/builtin/index.ts +++ b/packages/server/src/server/plugins/builtin/index.ts @@ -11,6 +11,7 @@ export const builtinPlugins = [ "grok-usage-source", "kimi-usage-source", "minimax-usage-source", + "muse-provider", "opencode-go-usage-source", "zai-usage-source", ] as const; diff --git a/packages/website/src/components/agent-icons.tsx b/packages/website/src/components/agent-icons.tsx index 7b6b66146d..8488050b12 100644 --- a/packages/website/src/components/agent-icons.tsx +++ b/packages/website/src/components/agent-icons.tsx @@ -82,3 +82,37 @@ export function PiIcon(props: React.SVGProps) { ); } + +export function MuseCodeIcon(props: React.SVGProps) { + return ( + + ); +} + +export function AntigravityIcon(props: React.SVGProps) { + return ( + + ); +} diff --git a/packages/website/src/components/landing-page.tsx b/packages/website/src/components/landing-page.tsx index d2d6684c93..6a29c62235 100644 --- a/packages/website/src/components/landing-page.tsx +++ b/packages/website/src/components/landing-page.tsx @@ -78,6 +78,8 @@ import { CursorIcon, OpenCodeIcon, PiIcon, + MuseCodeIcon, + AntigravityIcon, } from "~/components/agent-icons"; import { DiscordIcon, GitHubIcon, SlackIcon } from "~/components/brand-icons"; import { ClaudeIcon, MobileChat, MobileDiff, MobileSidebar, PhoneFrame } from "~/components/mockup"; @@ -161,6 +163,8 @@ function Hero({ title, subtitle }: { title: React.ReactNode; subtitle: React.Rea const CLAUDE_CODE_BADGE_ICON = ; const CODEX_BADGE_ICON = ; +const ANTIGRAVITY_BADGE_ICON = ; +const MUSE_CODE_BADGE_ICON = ; const OPENCODE_BADGE_ICON = ; const PI_BADGE_ICON = ; const CURSOR_BADGE_ICON = ; @@ -432,6 +436,8 @@ function MultiProviderSection() { { name: "Claude Code", icon: }, { name: "Codex", icon: }, { name: "OpenCode", icon: }, + { name: "Antigravity", icon: }, + { name: "Muse Code", icon: }, { name: "Pi", icon: }, { name: "Cursor", icon: }, ]; @@ -900,7 +906,8 @@ const PRINCIPLES: ReadonlyArray<{ icon: LucideIcon; title: string; description: { icon: Merge, title: "Unified", - description: "Claude Code, Codex, OpenCode, Pi, and more, all used the same way.", + description: + "Claude Code, Codex, OpenCode, Pi, Antigravity, Muse Code, and more, all used the same way.", }, { icon: Blocks, @@ -962,6 +969,8 @@ function GetStarted() { + + @@ -1207,10 +1216,15 @@ function FAQ() { Paseo supports many providers. It has custom implementations for Claude, Codex, OpenCode, - Pi, and OMP, and supports many more via ACP. See the full list here:{" "} + Pi, OMP, Antigravity, and Muse Code, and supports many more via ACP. See the full list + here:{" "} all supported providers + . See{" "} + + Muse Code setup and limitations + . diff --git a/packages/website/src/components/site-footer.tsx b/packages/website/src/components/site-footer.tsx index f73192c98a..ba092410da 100644 --- a/packages/website/src/components/site-footer.tsx +++ b/packages/website/src/components/site-footer.tsx @@ -86,6 +86,18 @@ export function SiteFooter({ width = "default" }: SiteFooterProps) { > OpenCode + + Antigravity + + + Muse Code + rootRouteImport, } as any); +const MuseCodeRoute = MuseCodeRouteImport.update({ + id: "/muse-code", + path: "/muse-code", + getParentRoute: () => rootRouteImport, +} as any); const MistralVibeRoute = MistralVibeRouteImport.update({ id: "/mistral-vibe", path: "/mistral-vibe", @@ -296,6 +303,11 @@ const AuggieRoute = AuggieRouteImport.update({ path: "/auggie", getParentRoute: () => rootRouteImport, } as any); +const AntigravityRoute = AntigravityRouteImport.update({ + id: "/antigravity", + path: "/antigravity", + getParentRoute: () => rootRouteImport, +} as any); const AmpRoute = AmpRouteImport.update({ id: "/amp", path: "/amp", @@ -384,6 +396,7 @@ export interface FileRoutesByFullPath { "/agents": typeof AgentsRoute; "/agoragentic": typeof AgoragenticRoute; "/amp": typeof AmpRoute; + "/antigravity": typeof AntigravityRoute; "/auggie": typeof AuggieRoute; "/autohand": typeof AutohandRoute; "/blog": typeof BlogRouteWithChildren; @@ -416,6 +429,7 @@ export interface FileRoutesByFullPath { "/kimi": typeof KimiRoute; "/minion-code": typeof MinionCodeRoute; "/mistral-vibe": typeof MistralVibeRoute; + "/muse-code": typeof MuseCodeRoute; "/nova": typeof NovaRoute; "/omp": typeof OmpRoute; "/opencode": typeof OpencodeRoute; @@ -447,6 +461,7 @@ export interface FileRoutesByTo { "/agents": typeof AgentsRoute; "/agoragentic": typeof AgoragenticRoute; "/amp": typeof AmpRoute; + "/antigravity": typeof AntigravityRoute; "/auggie": typeof AuggieRoute; "/autohand": typeof AutohandRoute; "/changelog": typeof ChangelogRoute; @@ -477,6 +492,7 @@ export interface FileRoutesByTo { "/kimi": typeof KimiRoute; "/minion-code": typeof MinionCodeRoute; "/mistral-vibe": typeof MistralVibeRoute; + "/muse-code": typeof MuseCodeRoute; "/nova": typeof NovaRoute; "/omp": typeof OmpRoute; "/opencode": typeof OpencodeRoute; @@ -509,6 +525,7 @@ export interface FileRoutesById { "/agents": typeof AgentsRoute; "/agoragentic": typeof AgoragenticRoute; "/amp": typeof AmpRoute; + "/antigravity": typeof AntigravityRoute; "/auggie": typeof AuggieRoute; "/autohand": typeof AutohandRoute; "/blog": typeof BlogRouteWithChildren; @@ -541,6 +558,7 @@ export interface FileRoutesById { "/kimi": typeof KimiRoute; "/minion-code": typeof MinionCodeRoute; "/mistral-vibe": typeof MistralVibeRoute; + "/muse-code": typeof MuseCodeRoute; "/nova": typeof NovaRoute; "/omp": typeof OmpRoute; "/opencode": typeof OpencodeRoute; @@ -574,6 +592,7 @@ export interface FileRouteTypes { | "/agents" | "/agoragentic" | "/amp" + | "/antigravity" | "/auggie" | "/autohand" | "/blog" @@ -606,6 +625,7 @@ export interface FileRouteTypes { | "/kimi" | "/minion-code" | "/mistral-vibe" + | "/muse-code" | "/nova" | "/omp" | "/opencode" @@ -637,6 +657,7 @@ export interface FileRouteTypes { | "/agents" | "/agoragentic" | "/amp" + | "/antigravity" | "/auggie" | "/autohand" | "/changelog" @@ -667,6 +688,7 @@ export interface FileRouteTypes { | "/kimi" | "/minion-code" | "/mistral-vibe" + | "/muse-code" | "/nova" | "/omp" | "/opencode" @@ -698,6 +720,7 @@ export interface FileRouteTypes { | "/agents" | "/agoragentic" | "/amp" + | "/antigravity" | "/auggie" | "/autohand" | "/blog" @@ -730,6 +753,7 @@ export interface FileRouteTypes { | "/kimi" | "/minion-code" | "/mistral-vibe" + | "/muse-code" | "/nova" | "/omp" | "/opencode" @@ -762,6 +786,7 @@ export interface RootRouteChildren { AgentsRoute: typeof AgentsRoute; AgoragenticRoute: typeof AgoragenticRoute; AmpRoute: typeof AmpRoute; + AntigravityRoute: typeof AntigravityRoute; AuggieRoute: typeof AuggieRoute; AutohandRoute: typeof AutohandRoute; BlogRoute: typeof BlogRouteWithChildren; @@ -794,6 +819,7 @@ export interface RootRouteChildren { KimiRoute: typeof KimiRoute; MinionCodeRoute: typeof MinionCodeRoute; MistralVibeRoute: typeof MistralVibeRoute; + MuseCodeRoute: typeof MuseCodeRoute; NovaRoute: typeof NovaRoute; OmpRoute: typeof OmpRoute; OpencodeRoute: typeof OpencodeRoute; @@ -910,6 +936,13 @@ declare module "@tanstack/react-router" { preLoaderRoute: typeof NovaRouteImport; parentRoute: typeof rootRouteImport; }; + "/muse-code": { + id: "/muse-code"; + path: "/muse-code"; + fullPath: "/muse-code"; + preLoaderRoute: typeof MuseCodeRouteImport; + parentRoute: typeof rootRouteImport; + }; "/mistral-vibe": { id: "/mistral-vibe"; path: "/mistral-vibe"; @@ -1134,6 +1167,13 @@ declare module "@tanstack/react-router" { preLoaderRoute: typeof AuggieRouteImport; parentRoute: typeof rootRouteImport; }; + "/antigravity": { + id: "/antigravity"; + path: "/antigravity"; + fullPath: "/antigravity"; + preLoaderRoute: typeof AntigravityRouteImport; + parentRoute: typeof rootRouteImport; + }; "/amp": { id: "/amp"; path: "/amp"; @@ -1278,6 +1318,7 @@ const rootRouteChildren: RootRouteChildren = { AgentsRoute: AgentsRoute, AgoragenticRoute: AgoragenticRoute, AmpRoute: AmpRoute, + AntigravityRoute: AntigravityRoute, AuggieRoute: AuggieRoute, AutohandRoute: AutohandRoute, BlogRoute: BlogRouteWithChildren, @@ -1310,6 +1351,7 @@ const rootRouteChildren: RootRouteChildren = { KimiRoute: KimiRoute, MinionCodeRoute: MinionCodeRoute, MistralVibeRoute: MistralVibeRoute, + MuseCodeRoute: MuseCodeRoute, NovaRoute: NovaRoute, OmpRoute: OmpRoute, OpencodeRoute: OpencodeRoute, diff --git a/packages/website/src/routes/agents.tsx b/packages/website/src/routes/agents.tsx index 884752b622..03548fe3bf 100644 --- a/packages/website/src/routes/agents.tsx +++ b/packages/website/src/routes/agents.tsx @@ -10,7 +10,7 @@ export const Route = createFileRoute("/agents")({ head: () => pageMeta( "Supported agents – Every coding agent Paseo runs", - "Run Claude Code, Codex, Copilot, OpenCode, Cursor CLI, Gemini CLI, and dozens more coding agents from your phone. Self-hosted, your code stays on your machine.", + "Run Claude Code, Codex, Copilot, OpenCode, Antigravity, Muse Code, Cursor CLI, Gemini CLI, and dozens more coding agents from your phone. Self-hosted, your code stays on your machine.", "/agents", ), component: AgentsPage, diff --git a/packages/website/src/routes/antigravity.tsx b/packages/website/src/routes/antigravity.tsx new file mode 100644 index 0000000000..3d821de7b9 --- /dev/null +++ b/packages/website/src/routes/antigravity.tsx @@ -0,0 +1,4 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { agentRouteOptions } from "~/components/agent-route"; + +export const Route = createFileRoute("/antigravity")(agentRouteOptions("antigravity")); diff --git a/packages/website/src/routes/muse-code.tsx b/packages/website/src/routes/muse-code.tsx new file mode 100644 index 0000000000..1a936492a9 --- /dev/null +++ b/packages/website/src/routes/muse-code.tsx @@ -0,0 +1,4 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { agentRouteOptions } from "~/components/agent-route"; + +export const Route = createFileRoute("/muse-code")(agentRouteOptions("muse-code")); diff --git a/plugins/muse-provider/client/settings.tsx b/plugins/muse-provider/client/settings.tsx new file mode 100644 index 0000000000..b057cf2cb0 --- /dev/null +++ b/plugins/muse-provider/client/settings.tsx @@ -0,0 +1,103 @@ +import { useCallback, useMemo } from "react"; +import { Text } from "react-native"; +import { useSettings, type PluginScreenProps, type SettingsState } from "@getpaseo/plugin/client"; +import { + SettingsAction, + SettingsCard, + SettingsSection, + SettingsSelect, + SettingsSwitch, +} from "@getpaseo/plugin/client/ui"; +import { museSettings } from "../shared/settings.js"; + +const networks = [ + { label: "Proxy only", value: "proxy-only" }, + { label: "Restricted", value: "restricted" }, + { label: "Enabled", value: "enabled" }, +] as const; +type ReadySettings = Extract, { status: "ready" }>; + +function Controls({ + settings, + theme, +}: { + settings: ReadySettings; + theme: PluginScreenProps["theme"]; +}) { + const style = useMemo(() => ({ color: theme.colors.foreground }), [theme]); + const changeSandbox = useCallback( + (sandbox: boolean) => { + void settings.save({ ...settings.values, sandbox }, settings.revision); + }, + [settings], + ); + const changeNetwork = useCallback( + (network: ReadySettings["values"]["network"]) => { + void settings.save({ ...settings.values, network }, settings.revision); + }, + [settings], + ); + const changeTrust = useCallback( + (trustWorkspace: boolean) => { + void settings.save({ ...settings.values, trustWorkspace }, settings.revision); + }, + [settings], + ); + return ( + + + + + + + + Changes apply to new Muse sessions. Disabling the sandbox enables full network access. Trust + workspace lets Muse load project skills and configuration. + + {settings.saveError ? ( + + {settings.saveError} + + ) : null} + + ); +} + +export function MuseSettings({ theme }: PluginScreenProps) { + const settings = useSettings(museSettings); + const style = useMemo(() => ({ color: theme.colors.foreground }), [theme]); + if (settings.status === "loading") return Loading settings…; + if (settings.status !== "ready") + return ( + + + {settings.error} + + + {settings.status === "invalid" ? ( + + ) : null} + + ); + return ; +} diff --git a/plugins/muse-provider/icon.svg b/plugins/muse-provider/icon.svg new file mode 100644 index 0000000000..4d0eb0cf45 --- /dev/null +++ b/plugins/muse-provider/icon.svg @@ -0,0 +1 @@ + diff --git a/plugins/muse-provider/index.client.tsx b/plugins/muse-provider/index.client.tsx new file mode 100644 index 0000000000..98db6de7a4 --- /dev/null +++ b/plugins/muse-provider/index.client.tsx @@ -0,0 +1,20 @@ +import type { PluginClientContext } from "@getpaseo/plugin/client"; +import { MuseSettings } from "./client/settings.js"; + +export default function contribute(client: PluginClientContext) { + client.addScreen({ + id: "muse", + title: "Muse Code", + Component: MuseSettings, + }); + client.addCommandCenterItem({ + id: "settings", + title: "Configure Muse Code", + icon: "Settings", + context: "global", + onSelect({ openScreen }) { + openScreen({ screenId: "muse" }); + }, + }); + return () => {}; +} diff --git a/plugins/muse-provider/index.server.ts b/plugins/muse-provider/index.server.ts new file mode 100644 index 0000000000..b5e988c23a --- /dev/null +++ b/plugins/muse-provider/index.server.ts @@ -0,0 +1,18 @@ +import type { PluginServerContext } from "@getpaseo/plugin/server"; +import { createMuseProvider } from "./server/provider.js"; + +import { museSettings } from "./shared/settings.js"; +import { Usage } from "./server/usage.js"; + +export default function contribute( + server: Pick< + PluginServerContext, + "registerProvider" | "registerSettings" | "registerUsageSource" + >, +) { + const settings = server.registerSettings(museSettings); + const usage = new Usage(); + server.registerUsageSource(usage.registration()); + server.registerProvider(createMuseProvider(settings, usage)); + return () => {}; +} diff --git a/plugins/muse-provider/paseo-plugin.json b/plugins/muse-provider/paseo-plugin.json new file mode 100644 index 0000000000..a53be3fef5 --- /dev/null +++ b/plugins/muse-provider/paseo-plugin.json @@ -0,0 +1 @@ +{ "id": "muse-provider", "requirements": { "paseo": ">=0.10.0" } } diff --git a/plugins/muse-provider/server/catalog.ts b/plugins/muse-provider/server/catalog.ts new file mode 100644 index 0000000000..d4e483ed79 --- /dev/null +++ b/plugins/muse-provider/server/catalog.ts @@ -0,0 +1,91 @@ +import type { ProviderCatalog, ProviderLaunch } from "@getpaseo/plugin/server/provider"; +import type { z } from "zod"; +import { createHash } from "node:crypto"; +import { MspConnection } from "./connection.js"; +import { catalogSchema, effortSchema } from "./wire.js"; + +export const modes = [ + { id: "onRequest", label: "Default", icon: "ShieldCheck", colorTier: "safe" }, + { id: "promptUnmatched", label: "Ask", icon: "ShieldAlert", colorTier: "moderate" }, + { id: "denyUnmatched", label: "Strict", icon: "ShieldCheck", colorTier: "safe" }, + { + id: "allowAll", + label: "Full access", + icon: "ShieldOff", + colorTier: "dangerous", + isUnattended: true, + }, +]; +export const efforts = effortSchema.options.filter((effort) => effort !== "none"); + +export function launchKey(launch: ProviderLaunch): string { + // Hash values so credential-dependent cache identity never exposes credentials. + const routingKeys = Object.keys(launch.env) + .filter((key) => /^(META_|MUSE_|XDG_|HOME$|PATH$|HTTP_PROXY$|HTTPS_PROXY$|NO_PROXY$)/.test(key)) + .sort(); + const environment = routingKeys.map((key) => [key, launch.env[key]]); + return createHash("sha256") + .update(JSON.stringify([launch.command, launch.args, environment])) + .digest("hex"); +} + +export class Catalog { + private readonly cache = new Map(); + private readonly hosts = new Set(); + private closed = false; + async read(launch: ProviderLaunch): Promise { + if (this.closed) throw new Error("Muse catalogue is closed"); + const key = launchKey(launch); + const cached = this.cache.get(key); + if (cached && cached.expires > Date.now()) return cached.catalog; + const host = new MspConnection({ launch, timeoutMs: 5000 }); + this.hosts.add(host); + try { + await host.initialize(); + const response = await host.request("model/list", {}, catalogSchema); + const catalog = presentCatalog(response); + if (!this.closed) { + if (this.cache.size >= 8) this.cache.delete(this.cache.keys().next().value!); + this.cache.set(key, { expires: Date.now() + 60000, catalog }); + } + return catalog; + } finally { + await host.close(); + this.hosts.delete(host); + } + } + async close(): Promise { + this.closed = true; + this.cache.clear(); + await Promise.all([...this.hosts].map((host) => host.close())); + } +} + +export function presentCatalog(response: z.infer): ProviderCatalog { + const models = response.models.map((model) => { + let supported: z.infer[] = efforts; + if (Array.isArray(model.reasoningEffortVariants) && model.reasoningEffortVariants.length > 0) + supported = model.reasoningEffortVariants; + else if (model.variants.length > 0) supported = model.variants; + return { + id: model.modelId, + label: model.displayLabel, + isDefault: model.isDefault, + contextWindowMaxTokens: model.contextLimit ?? undefined, + thinkingOptions: supported.map((id) => ({ + id, + label: id, + isDefault: id === model.defaultReasoningEffort, + })), + defaultThinkingOptionId: model.defaultReasoningEffort ?? undefined, + metadata: { providerId: model.providerId }, + }; + }); + const defaultModel = models.find((model) => model.isDefault); + return { + models, + modes, + defaultMode: "onRequest", + defaultModel: defaultModel?.id, + }; +} diff --git a/plugins/muse-provider/server/children.ts b/plugins/muse-provider/server/children.ts new file mode 100644 index 0000000000..69e0f4a855 --- /dev/null +++ b/plugins/muse-provider/server/children.ts @@ -0,0 +1,94 @@ +import type { ProviderEvent } from "@getpaseo/plugin/server/provider"; +import { MspConnection, type Notification } from "./connection.js"; +import { Timeline } from "./timeline.js"; +import { + childSessionSchema, + pageSchema, + itemNotificationSchema, + notificationSchema, + deltaSchema, + type WireItem, +} from "./wire.js"; + +interface Child { + timeline: Timeline; + cursor: string | undefined; +} +export class Children { + private readonly children = new Map(); + constructor( + private readonly host: MspConnection, + private readonly parentId: string, + private readonly cwd: string, + private readonly emit: (event: ProviderEvent) => void, + ) {} + + async update(item: WireItem): Promise { + if ( + !item.childSessionId || + !( + item.kind === "subagent" || + item.kind === "workflow" || + item.tool === "workflow" || + item.tool.startsWith("subagent_") + ) + ) + return; + const id = item.childSessionId; + if (!this.children.has(id)) { + const { session } = await this.host.request( + "session/read", + { sessionId: id }, + childSessionSchema, + ); + this.emit({ + type: "session.opened", + sessionId: id, + parentSessionId: this.parentId, + toolCallId: item.callId ?? item.itemId, + capabilities: [], + restoration: "parent", + cwd: session.workspaceRoot ?? this.cwd, + title: session.title, + }); + this.children.set(id, { + timeline: new Timeline(this.host, id, id, this.emit), + cursor: undefined, + }); + } + await this.page(id); + } + async notify(id: string, event: Notification): Promise { + const child = this.children.get(id); + if (!child) return; + if (["item/started", "item/updated", "item/completed"].includes(event.method)) { + const { item } = itemNotificationSchema.parse(event.params); + await child.timeline.fold(item); + } else if (event.method === "item/delta") child.timeline.delta(deltaSchema.parse(event.params)); + } + async refresh(): Promise { + for (const id of this.children.keys()) await this.page(id); + } + private async page(id: string): Promise { + const child = this.children.get(id)!; + let next: string | null; + do { + const page = await this.host.request( + "view/page", + { sessionId: id, cursor: child.cursor, limit: 1000, direction: "forward" }, + pageSchema, + ); + for (const event of page.events) { + await this.notify(id, event); + const envelope = notificationSchema.parse(event.params); + if (envelope.viewCursor) child.cursor = envelope.viewCursor; + } + next = page.nextCursor; + if (next !== null) child.cursor = next; + } while (next !== null); + } + close(): void { + for (const sessionId of this.children.keys()) this.emit({ type: "session.closed", sessionId }); + this.children.clear(); + } +} diff --git a/plugins/muse-provider/server/commands.ts b/plugins/muse-provider/server/commands.ts new file mode 100644 index 0000000000..df62f72537 --- /dev/null +++ b/plugins/muse-provider/server/commands.ts @@ -0,0 +1,56 @@ +import type { ProviderEvent, ProviderPrompt } from "@getpaseo/plugin/server/provider"; +import type { TurnStartParams } from "./msp.js"; +import { MspConnection } from "./connection.js"; +import { ackSchema, skillsSchema } from "./wire.js"; +import { MuseError } from "./errors.js"; + +export class Commands { + private selectors = new Set(); + constructor( + private readonly host: MspConnection, + private readonly sessionId: string, + private readonly id: string, + private readonly emit: (event: ProviderEvent) => void, + ) {} + + async refresh(): Promise { + const { skills } = await this.host.request( + "skill/list", + { sessionId: this.sessionId }, + skillsSchema, + ); + this.selectors = new Set(skills.map((skill) => skill.selector)); + this.emit({ + type: "session.commands", + sessionId: this.id, + commands: [ + { name: "compact", description: "Compact conversation context" }, + ...skills + .filter((skill) => skill.selector !== "compact") + .map((skill) => ({ + name: skill.selector, + description: skill.description, + argumentHint: skill.argumentHint, + })), + ], + }); + } + async compact(prompt: ProviderPrompt): Promise { + if (prompt.input.type !== "command" || prompt.input.name !== "compact") return false; + await this.host.command("session/compact", { sessionId: this.sessionId }, ackSchema); + this.emit({ + type: "session.prompt_result", + sessionId: this.id, + clientMessageId: prompt.clientMessageId, + result: { type: "completed" }, + }); + return true; + } + input( + command: Extract, + ): TurnStartParams["input"][number] { + if (!this.selectors.has(command.name)) + throw new MuseError("unknownCommand", `Muse skill is unavailable: ${command.name}`); + return { type: "skill", selector: command.name, arguments: command.arguments }; + } +} diff --git a/plugins/muse-provider/server/connection.ts b/plugins/muse-provider/server/connection.ts new file mode 100644 index 0000000000..100bd101c7 --- /dev/null +++ b/plugins/muse-provider/server/connection.ts @@ -0,0 +1,193 @@ +import type { ProviderLaunch } from "@getpaseo/plugin/server/provider"; +import { spawn } from "node:child_process"; +import { randomBytes } from "node:crypto"; +import { createInterface } from "node:readline"; +import { setTimeout as delay } from "node:timers/promises"; +import { z } from "zod"; +import { MuseError, classifyExit } from "./errors.js"; +import { frameSchema, initializedSchema, fingerprint } from "./wire.js"; + +export function commandId(): string { + const bytes = randomBytes(16); + bytes.writeUIntBE(Date.now(), 0, 6); + bytes[6] = 0x70 | (bytes[6]! & 0x0f); + bytes[8] = 0x80 | (bytes[8]! & 0x3f); + const hex = bytes.toString("hex"); + return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`; +} + +interface PendingRequest { + resolve(value: unknown): void; + reject(error: Error): void; + timer: ReturnType; +} +export interface Notification { + method: string; + params: unknown; +} + +export class MspConnection { + private readonly child; + private readonly pending = new Map(); + private readonly listeners = new Set<(notification: Notification) => void>(); + private readonly exitListeners = new Set<(error: MuseError) => void>(); + private sequence = 0; + private stderr = ""; + private failure: MuseError | null = null; + private closing = false; + private readonly exited: Promise; + + constructor( + private readonly options: { + launch: ProviderLaunch; + cwd?: string; + timeoutMs?: number; + serveArgs?: string[]; + }, + ) { + const { launch, cwd } = options; + this.child = spawn(launch.command, [...launch.args, "serve", ...(options.serveArgs ?? [])], { + env: launch.env, + cwd, + stdio: "pipe", + }); + this.child.stderr.setEncoding("utf8"); + this.child.stderr.on("data", (chunk: string) => { + this.stderr = (this.stderr + chunk).slice(-8192); + }); + const lines = createInterface({ input: this.child.stdout }); + lines.on("line", (line) => { + try { + this.receive(frameSchema.parse(JSON.parse(line))); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + this.fail(new MuseError("invalidFrame", `Invalid MSP frame: ${message}`)); + this.child.kill(); + } + }); + this.child.stdin.on("error", () => { + /* Process exit owns the diagnostic and pending requests. */ + }); + this.child.on("error", (error) => this.fail(new MuseError("spawn", error.message))); + this.exited = new Promise((resolve) => { + this.child.once("close", (code, signal) => { + lines.close(); + this.fail(classifyExit(code, signal, this.stderr)); + resolve(); + }); + }); + } + + async initialize(): Promise { + const response = await this.request( + "initialize", + { + clientInfo: { name: "paseo", version: "1" }, + capabilities: { + experimentalApi: true, + requestedCapabilities: ["sessionMcp", "sessionListStream"], + }, + }, + initializedSchema, + ); + if (response.schema.fingerprint !== fingerprint) { + process.stderr.write( + `Muse MSP fingerprint mismatch: expected ${fingerprint}, received ${response.schema.fingerprint}\n`, + ); + } + this.write({ jsonrpc: "2.0", method: "initialized", params: {} }); + } + + onNotification(listener: (notification: Notification) => void): () => void { + this.listeners.add(listener); + return () => this.listeners.delete(listener); + } + onExit(listener: (error: MuseError) => void): () => void { + this.exitListeners.add(listener); + return () => this.exitListeners.delete(listener); + } + async command(method: string, params: P, schema: z.ZodType): Promise { + const id = + "commandId" in params && typeof params.commandId === "string" + ? params.commandId + : commandId(); + const input = { ...params, commandId: id }; + for (let attempt = 0; ; attempt++) { + try { + return await this.request(method, input, schema); + } catch (error) { + const retryable = + error instanceof MuseError && ["overloaded", "backpressured"].includes(error.kind); + if (!retryable || attempt === 2) throw error; + await delay(100 * (attempt + 1)); + } + } + } + async request(method: string, params: object, schema: z.ZodType): Promise { + if (this.failure) throw this.failure; + if (this.closing) throw new MuseError("closed", "Muse host is closing"); + const id = ++this.sequence; + const response = await new Promise((resolve, reject) => { + const timer = setTimeout(() => { + this.pending.delete(id); + reject(new MuseError("timeout", `Muse ${method} timed out`)); + }, this.options.timeoutMs ?? 10000); + this.pending.set(id, { resolve, reject, timer }); + this.write({ jsonrpc: "2.0", id, method, params }); + }); + return schema.parse(response); + } + async close(): Promise { + if (this.closing) return this.exited; + this.closing = true; + this.child.stdin.end(); + const drained = await Promise.race([ + this.exited.then(() => true), + delay(1000).then(() => false), + ]); + if (!drained) this.child.kill("SIGKILL"); + await this.exited; + } + private write(frame: object): void { + this.child.stdin.write(JSON.stringify(frame) + "\n"); + } + private receive(frame: z.infer): void { + if (frame.method) { + if (frame.id !== undefined) { + if (["approval/request", "userInput/request"].includes(frame.method)) { + this.write({ jsonrpc: "2.0", id: frame.id, result: {} }); + } else { + this.write({ + jsonrpc: "2.0", + id: frame.id, + error: { code: -32601, message: "Unsupported server request" }, + }); + } + return; + } + for (const listener of this.listeners) + listener({ method: frame.method, params: frame.params }); + return; + } + if (frame.id === undefined) throw new MuseError("invalidFrame", "MSP response has no id"); + const pending = this.pending.get(frame.id); + if (!pending) return; + clearTimeout(pending.timer); + this.pending.delete(frame.id); + if (frame.error) { + let kind = frame.error.data?.kind ?? "rpc"; + if (frame.error.code === -32020) kind = "sessionNotFound"; + pending.reject(new MuseError(kind, frame.error.message, undefined, frame.error.data?.reason)); + } else pending.resolve(frame.result); + } + private fail(error: MuseError): void { + if (this.failure) return; + this.failure = error; + for (const pending of this.pending.values()) { + clearTimeout(pending.timer); + pending.reject(error); + } + this.pending.clear(); + if (!this.closing) for (const listener of this.exitListeners) listener(error); + } +} diff --git a/plugins/muse-provider/server/errors.ts b/plugins/muse-provider/server/errors.ts new file mode 100644 index 0000000000..86f6254af6 --- /dev/null +++ b/plugins/muse-provider/server/errors.ts @@ -0,0 +1,81 @@ +import type { ProviderError, ProviderLaunch } from "@getpaseo/plugin/server/provider"; +import path from "node:path"; + +export class MuseError extends Error { + constructor( + readonly kind: string, + message: string, + readonly diagnostic?: string, + readonly reason?: string, + ) { + super(message); + this.name = "MuseError"; + } + toProviderError(): ProviderError { + return { code: this.kind, message: this.message, diagnostic: this.diagnostic }; + } +} + +export function classifyExit( + code: number | null, + signal: string | null, + stderr: string, +): MuseError { + const kinds: Record = { + 0: "cleanExit", + 2: "usage", + 3: "credentials", + 4: "leaseHeld", + 5: "surfaceOff", + }; + const kind = code === null ? "crash" : (kinds[code] ?? "crash"); + return new MuseError(kind, `Muse host exited (${code ?? signal}). ${stderr}`); +} + +function isUnavailableReviewer(message: string): boolean { + return message.includes("automated reviewer is unavailable"); +} + +export function actionableError(error: unknown, launch: ProviderLaunch): ProviderError { + if (!(error instanceof Error)) return { code: "muse", message: String(error) }; + // Muse 1.4.1 reports this profile failure as internal, without a specific error kind. + if ( + error instanceof MuseError && + error.kind === "internal" && + isUnavailableReviewer(error.message) + ) { + const configHome = launch.env.XDG_CONFIG_HOME ?? path.join(launch.env.HOME ?? "~", ".config"); + return { + code: "defaultProfileUnavailable", + message: `Change permissions.default_profile in ${path.join(configHome, "muse/settings.json")}: the automated reviewer is unavailable on this host.`, + }; + } + if (error instanceof MuseError && error.reason === "compaction_unavailable") { + return { + code: error.kind, + message: + "Muse cannot compact this session with its current model configuration. Ensure Muse has a context limit for the selected model (context_compaction.provider_context_limit_tokens).", + diagnostic: error.message, + }; + } + if (error instanceof MuseError && error.kind === "sessionNotFound") { + return { code: error.kind, message: "This Muse session no longer exists. Create a new agent." }; + } + if (error instanceof MuseError && error.kind === "authRequired") { + return { code: error.kind, message: "Run `muse login` or set META_API_KEY." }; + } + // Muse 1.4.1 reports this bootstrap failure as internal, without a specific error kind. + if ( + error instanceof MuseError && + error.kind === "internal" && + error.message.includes("UnsafePath") + ) { + const dataHome = launch.env.XDG_DATA_HOME ?? path.join(launch.env.HOME ?? "~", ".local/share"); + return { + code: "unsafePath", + message: `Muse data directory ${path.join(dataHome, "muse")} requires mode 0700. Set its permissions before retrying.`, + }; + } + if (error instanceof MuseError) return error.toProviderError(); + return { code: "muse", message: error.message }; +} diff --git a/plugins/muse-provider/server/msp.d.ts b/plugins/muse-provider/server/msp.d.ts new file mode 100644 index 0000000000..8f2180932d --- /dev/null +++ b/plugins/muse-provider/server/msp.d.ts @@ -0,0 +1,2690 @@ +// Vendored from Muse Code 1.4.1 via muse schema generate-ts. +// Stable fingerprint: sha256:e0e163db6ccf00dbe68402ce55d6319b3edc33c421f31e9583b587b2de8a118f +// Muse Session Protocol (MSP) v1 TypeScript declarations. +// STABLE surface only; experimental twin: experimental/msp.d.ts. +// Generated by tbh-protocol (spec 206) from the same schema model as +// the JSON Schema bundle (INV-017). Do not edit by hand. + +export interface ApprovalAmendment { + durability: ApprovalAmendmentDurability; + rulePreview: string; +} + +export type ApprovalAmendmentDurability = "session" | "localPersistent" | (string & {}); + +export interface ApprovalChange { + choiceId?: string; + decision?: ApprovalDecision; + executable?: boolean; + kind: string; + reason?: string; + reparsed?: boolean; + requirementId?: ApprovalRequirementRef; + status?: ApprovalPersistenceStatus; +} + +export interface ApprovalChoice { + acceptsFeedback?: boolean; + choiceId: string; + decision: ApprovalDecision; + label: string; + rulePreview?: string; + scope: ApprovalChoiceScope; +} + +export type ApprovalChoiceScope = "once" | "session" | "localPersistent" | (string & {}); + +/** `approval/decide` params (tdd SS5.4): the decision. A standard SS3 command — requires the session loaded on this host, requires `commandId`, durable intake before ack, value-identical replay. */ +export interface ApprovalDecideParams { + /** The approval being decided. */ + approvalId: string; + /** One of the current `availableChoices`; else -32052. */ + choiceId: string; + /** The SS3.1.1 idempotency handle (UUIDv7). */ + commandId: string; + /** Free-text guidance delivered to the model with the denial. Only valid on choices with `acceptsFeedback`. Never persisted in the durable audit record (live steer only), so it is absent from `approval/resolved` (tdd SS5.4). Omitted and explicit `null` both mean "no feedback" (#23468). */ + feedback?: string | null; + /** Must equal the approval's `currentRequirementId`. The multi-stage race guard: a decision aimed at stage 1 can never accidentally satisfy stage 2, and a stale value is -32053 (tdd SS5.4). */ + requirementId: ApprovalRequirementRef; + /** The target session. */ + sessionId: string; +} + +/** `approval/decide` result (tdd SS5.4). Admission-ack rules still apply: the authoritative outcome is `approval/resolved` / `approval/updated` on the view stream. */ +export interface ApprovalDecideResult { + /** The approval this decision was applied to. */ + approvalId: string; + /** Echoes the client's id. */ + commandId: string; + /** Admission status. */ + status: CommandStatus; + /** `false` when the choice satisfied a stage but further requirements remain (the approval stays pending and an updated `approval/request` follows); `true` when this decision produced the terminal `DecisionApplied` (tdd SS5.4). */ + terminal: boolean; +} + +export type ApprovalDecision = + | "approved" + | "approvedForSession" + | "approvedPolicyAmendment" + | "denied" + | "deniedPolicyAmendment" + | "timedOut" + | "abort" + | (string & {}); + +/** `approval/listPending` params (tdd SS5.7): the pull dual of the re-issued requests. A log-fold read — no lease, works on loaded and unloaded sessions, never subscribes. */ +export interface ApprovalListPendingParams { + /** The session to read. */ + sessionId: string; +} + +/** `approval/listPending` result (tdd SS5.7). Ordering is by opening `viewCursor`; empty arrays when nothing is pending. The result is point-in-time — to act on it race-safely, `approval/decide` carries the `requirementId` guard regardless of how the client learned the state. */ +export interface ApprovalListPendingResult { + /** Exactly the `approval/request` params of tdd SS5.3, one per pending approval. */ + approvals: ApprovalRequestParams[]; + /** Exactly the `userInput/request` params of tdd SS5.10, one per pending prompt. */ + userInputs: UserInputRequestParams[]; +} + +/** The approval enforcement modes (tdd SS5.12, camelCased `ApprovalBackendMode`). **Closed** by design (D-006, select-never-create): a client selects a preconfigured mode and can never construct one; widening this enum is a deliberate, gate-visible protocol change. */ +export type ApprovalMode = "allowAll" | "promptUnmatched" | "onRequest" | "denyUnmatched"; + +/** Whether a mode change did anything (tdd SS5.12), mirroring `ApprovalReconfigureApplyOutcome`. Apply failures are `commandRejected`. */ +export type ApprovalModeApplyOutcome = "completed" | "noop" | (string & {}); + +/** How an approval mode took effect (tdd SS5.12, the effective-mode projection's `source` vocabulary). Open. */ +export type ApprovalModeSource = "startup" | "replay" | "approvalReconfigure" | (string & {}); + +export interface ApprovalOrigin { + command?: string; + kind: string; + url?: string; +} + +export type ApprovalPersistenceStatus = "succeeded" | "failed" | (string & {}); + +export type ApprovalPolicyResult = "allow" | "deny" | (string & {}); + +/** Full params shared by `approval/request` and `approval/requested`. */ +export interface ApprovalRequestParams { + approvalId: string; + availableChoices: ApprovalChoice[]; + currentRequirementId: ApprovalRequirementRef; + itemId: string; + judgeEscalated: boolean; + protectedWrite: boolean; + rawArgs: string; + sessionId: string; + sourceRange: SourceRange; + /** Present only for a child (native subagent) approval projected onto the parent session's plane (ADR 36182); parent-own approvals omit it. */ + subagentOrigin?: ApprovalSubagentOrigin; + subject: ApprovalSubject; + taskId: string; + toolCallId: string; + toolName: string; + turnId: string; + viewCursor: string; +} + +/** Approval-stage token carried in SS5 params and stale-requirement errors. */ +export interface ApprovalRequirementRef { + approvalId: string; + sourceIndex: number; +} + +/** Winning terminal returned to a losing `approval/decide` command. */ +export interface ApprovalResolutionSummary { + decision: string; + resolvedBy: string; + viewCursor: string; +} + +export type ApprovalResolvedBy = "user" | "policy" | "llmJudge" | (string & {}); + +/** `approval/resolved` params: the first durable terminal decision. */ +export interface ApprovalResolvedParams { + amendment?: ApprovalAmendment; + approvalId: string; + decidedByCommandId?: string; + decision: ApprovalDecision; + itemId: string; + policyResult: ApprovalPolicyResult; + resolvedBy: ApprovalResolvedBy; + sessionId: string; + sourceRange: SourceRange; + stageEvidence: ApprovalStageEvidence[]; + /** Present only for a child (native subagent) approval projected onto the parent session's plane (ADR 36182); parent-own approvals omit it. */ + subagentOrigin?: ApprovalSubagentOrigin; + turnId: string; + viewCursor: string; +} + +export interface ApprovalStage { + argv: string[]; + argvComplete: boolean; + position: number; + requirementId: ApprovalRequirementRef; + resolution: ApprovalStageResolution; + suggestedPrefix?: ApprovalSuggestedPrefix; + totalStages: number; +} + +export interface ApprovalStageEvidence { + argv: string[]; + position: number; + requirementId: ApprovalRequirementRef; + resolution: ApprovalStageResolution; + totalStages: number; +} + +export interface ApprovalStageResolution { + argvPrefix?: string[]; + diagnostic?: string; + kind: string; +} + +/** Child (native subagent) provenance for an approval projected onto the PARENT session's approval plane (#36182 / ADR 36182): identifies the originating child run/agent so a client can attribute the prompt. Omitted (`skip_serializing_if`) on the parent's own approvals, so pre-existing parent frames stay byte-identical. */ +export interface ApprovalSubagentOrigin { + /** The child run that parked on this approval. */ + childRunId: string; + /** The child session stream id whose records carry this approval. */ + childSessionId: string; + /** The originating child's subagent id, from the parent session's durable subagent control records. */ + subagentId: string; +} + +/** Open approval subject union. Unknown kinds are rendered generically and never auto-approved by clients (tdd SS5.2). */ +export interface ApprovalSubject { + access?: string; + command?: string; + host?: string; + /** Open discriminator: `shell | fileAccess | network | unixSocket | process | tool`. */ + kind: string; + origin?: ApprovalOrigin; + path?: string; + port?: number; + protocol?: string; + stages?: ApprovalStage[]; + target?: string; + toolName?: string; + workspaceRoot?: string; +} + +export interface ApprovalSuggestedPrefix { + argvPrefix: string[]; + label: string; +} + +/** `approval/updated` params: the refreshed pending view plus one change. */ +export interface ApprovalUpdatedParams { + approvalId: string; + availableChoices: ApprovalChoice[]; + change: ApprovalChange; + currentRequirementId: ApprovalRequirementRef; + sessionId: string; + sourceRange: SourceRange; + /** Present only for a child (native subagent) approval projected onto the parent session's plane (ADR 36182); parent-own approvals omit it. */ + subagentOrigin?: ApprovalSubagentOrigin; + subject: ApprovalSubject; + viewCursor: string; +} + +/** One attention flag (tdd SS2.4, ADR 31983 D3): a pending server-initiated request class parked on the session. Open on the wire — clients MUST ignore unknown values; future attention kinds are additive. */ +export type AttentionFlag = "approvalPending" | "inputPending" | (string & {}); + +/** Who durably backgrounded a task (tdd SS4.5.5, `TaskBackgroundedInitiator`). Open — runtime vocabulary. */ +export type BackgroundInitiator = "user" | "timeout" | (string & {}); + +/** The latest branch observation in the snapshot (tdd SS4.6.4, SS4.9.1). */ +export interface BranchState { + /** The observed branch; `null` on a detached-HEAD observation. */ + branch: string | null; + /** The detected version-control system; absent when none was detected. */ + vcs?: Vcs; + /** The observed workspace root. */ + workspaceRoot: string; +} + +/** A grantable capability name (SS1.4.4). Open: the reserved `rawLog` entry joins this domain as an additive open-enum extension when SS6 un-defers (#13929, Scenario 5 AS-3) — closed would make that a retype. */ +export type CapabilityName = "userShell" | "sessionMcp" | "sessionListStream" | (string & {}); + +/** The client's requested capability posture (SS1.4.1). Every member defaults; an absent `capabilities` object means all defaults. */ +export interface ClientCapabilities { + /** Opt into experimental methods, fields, and variants (default `false`). */ + experimentalApi?: boolean; + /** Exact notification method names the client declines; no wildcards; unknown names accepted and ignored; the protected set (SS1.7) cannot be opted out. */ + optOutNotificationMethods?: string[]; + /** Names from the capability registry. Unknown entries are not an error and simply do not appear in `grantedCapabilities` — which is why this is a list of free strings rather than of registry names. */ + requestedCapabilities?: string[]; + /** Whether this client can render a `userInput/request` dialog and answer it (SS5.10). **Absent means capable** (D-047): a client that predates the member keeps today's contract, so no existing client changes its handshake. `false` makes the host withhold `userInput/request` toward this client — the D-027 additive-optional-field + withheld-method convention. Deliberately NOT a `requestedCapabilities` name: that registry records operations a host GRANTS (SS1.4.4), and whether a client can draw a dialog is a fact about the client, never a privilege the host hands out. */ + userInputDialogs?: boolean; +} + +/** Client identification inside `initialize` params (SS1.4.1). Diagnostics and telemetry attribution only; never an authority claim (SS1.10). */ +export interface ClientInfo { + /** Machine identifier, `[a-z0-9_]+` (SS1.4.1). */ + name: string; + /** Human display name. */ + title?: string; + /** Client version string. */ + version: string; +} + +/** The uniform SS3.1.2 command acknowledgement: admission only, never an outcome. `session/compact` alone may answer `"noop"`; every other command answers `"accepted"`. */ +export interface CommandAcceptedResult { + /** Echo of the client-minted UUIDv7 `commandId` (SS3.1.1). */ + commandId: string; + /** `"accepted"` for every admitted command (SS3.1.2; closed vocabulary). */ + status: CommandAckStatus; +} + +/** The ack-status vocabulary (`tdd.md` SS3.1/SS3.1.2): `"accepted"` for every admitted command; `session/compact` alone may answer `"noop"`. Closed: SS3.1.2 names exactly these two values, so SDK clients get a discriminated type and validation catches a wrong status (PR #21550 review) — and a client that received a third value would have been told nothing it can act on, unlike [`TurnStartDisposition`], where "acked, not otherwise classified" is a usable reading. */ +export type CommandAckStatus = "accepted" | "noop"; + +/** The SS3.1.2 ack `status`: `accepted` for every admitted command. `session/compact` is the one method that may additionally answer `noop` ([`crate::method::runtime::CompactStatus`]); no other command uses another status (tdd SS3.1.2). */ +export type CommandStatus = "accepted" | (string & {}); + +/** `session/compact`'s ack status (tdd SS3.7): the one method that may answer `noop` in addition to the SS3.1.2 `accepted`. A noop is a success, not an error, and it is durably settled like any other outcome. */ +export type CompactStatus = "accepted" | "noop" | (string & {}); + +/** Compaction outcome (tdd SS4.5.10, the SS3.7 vocabulary). Open. */ +export type CompactionOutcome = "compacted" | "noop" | "failed" | "cancelled" | (string & {}); + +/** What initiated a compaction (tdd SS4.5.10). Open. */ +export type CompactionTrigger = "manual" | "auto" | (string & {}); + +/** Context pressure level (tdd SS4.6.6): hard threshold first, both inclusive `>=`. Open. */ +export type ContextPressureLevel = "normal" | "warning" | "blocked" | (string & {}); + +/** The SS4.9.1 snapshot `contextUsage` block: the latest `(windowTokens, usedTokens, pressure)` triple; the snapshot member is `null` until the fold's generation chain holds a tracked anchor AND the current basis is present (absent is never fabricated). */ +export interface ContextUsage { + /** Server-computed pressure level. */ + pressure: ContextPressureLevel; + /** Counted-once occupancy at the latest provider-reported fact. */ + usedTokens: number; + /** The effective context-window size; absent when the basis has no limit. */ + windowTokens?: number; +} + +/** Session running totals of counted-once usage (tdd SS4.6.5). */ +export interface CumulativeTokenUsage { + /** Output tokens, session total. */ + outputTokens: number; + /** Counted-once prompt tokens, session total. */ + promptTokens: number; + /** Counted-once totals, session total. */ + totalTokens: number; +} + +/** The effective approval-mode projection (`EffectiveApprovalModeState`, tdd SS5.12): the same object `session/setApprovalMode` returns as `effectiveMode` and the `Session` object carries as `approvalMode`. */ +export interface EffectiveApprovalModeState { + /** The command that last set it; `null` when no command did. */ + lastCommandId: string | null; + /** The mode in effect. */ + mode: ApprovalMode; + /** How the mode took effect. */ + source: ApprovalModeSource; +} + +/** The latest effective-model fact in the snapshot (tdd SS4.9.1). */ +export interface EffectiveModel { + /** The selected model. */ + modelId: string; + /** The selected provider; `null` when the selection names none. */ + providerId: string | null; + /** What drove the selection. */ + source: ModelChangeSource; +} + +/** The optional `error.data` object (SS1.6). All members additive-optional; `kind` is always present when `data` is. */ +export interface ErrorData { + /** The next provable UTF-8 boundary on a misaligned text-media `item/readOutput` read (`-32602` `invalidParams`, FR-016b/D-17214-2): absent when no boundary is provable from the stored bytes ahead. */ + alignedNextOffset?: number; + /** The rejected anchor value on `notFound`/`missingAnchor` and the `-32042` boundary arms (SS4.7.3, SS4.11); on `-32042` it is served as an explicit `null` when the request named no concrete anchor (spec 208 FM-005). */ + anchor?: string | null; + /** Approval identity on SS5 errors. */ + approvalId?: string; + /** The stored output's availability on `outputUnavailable` — the durable `ToolOutputAvailability` value, read-time-derived when the durable record still says `available` (SS4.5.5; spec 208 FM-008). */ + availability?: string; + /** The missing capability, on `capabilityRequired` errors. Typed on the grantable registry, so `rawLog` is outside the v1 domain and joins it as an additive open-enum extension when SS6 un-defers (TEST-009). */ + capability?: CapabilityName; + /** The configured command-admission bound, on `backpressured` errors. */ + capacity?: number; + /** Rejected server-minted choice on `approvalChoiceInvalid`. */ + choiceId?: string; + /** The rejected command's identifier, on `commandRejected` errors. The ratified v1 contract pairs it with `reason` (specs/13929-msp-activation/tdd.md SS3.6 admission-failure table and its `-32030` example), so a client can settle the exact command it sent without parsing the message string. */ + commandId?: string; + /** Current stage token on `approvalRequirementStale`. */ + currentRequirementId?: ApprovalRequirementRef; + /** SS1.5.1 descriptor, on `experimentalRequired` errors. */ + descriptor?: string; + /** Free-form additive detail. */ + details?: Record; + /** The oldest servable view position on `viewTruncated` (`-32040`, SS4.11); served as an explicit `null` when nothing before the head is servable — the null is present on the wire, never a dropped key (spec 208 FM-005 / Scenario 6.2). */ + earliestCursor?: string | null; + /** The item whose stored output was requested, on the `item/readOutput` arms (`notFound`, `outputUnavailable`, and the misaligned-offset `invalidParams`). */ + itemId?: string; + /** The stable camelCase category clients branch on. */ + kind: ErrorKind; + /** Rejected inclusive fork boundary on `forkBoundaryInvalid`. */ + lastTurnId?: string; + /** The latest installed boundary's view cursor on the `-32042` arms — always present in the `-32042` JSON and explicitly `null` when no boundary exists (SS4.11, FM-004). */ + latestBoundaryCursor?: string | null; + /** The exceeded byte limit, on `inputTooLarge` errors. */ + limitBytes?: number; + /** The requested `outputRef.id`, on the same `item/readOutput` arms. */ + outputRef?: string; + /** Candidate retained paths on `sessionAmbiguous`. */ + paths?: string[]; + /** Failure detail vocabulary (e.g. `"missingAnchor"` on `notFound` for a missing view-cursor anchor). */ + reason?: string; + /** Winning resolution on `approvalAlreadyResolved`. */ + resolution?: ApprovalResolutionSummary; + /** Whether retrying can succeed (e.g. `overloaded`). When present, overrides the error table row's `retryable` default for this code. */ + retryable?: boolean; + /** The rejected skill selector on `skillNotFound` (`-32032`, SS3.22.4). */ + selector?: string; + /** Session identity on errors whose lookup is session-scoped. */ + sessionId?: string; + /** Winning settlement on `userInputAlreadySettled`. */ + settlement?: UserInputSettlementSummary; + /** User-input prompt identity on SS5.10 errors. */ + userInputId?: string; + /** The blocked view position on `pageEventTooLarge` errors. */ + viewCursor?: string; +} + +/** A stable `error.data.kind` category (SS1.6): camelCase, the value clients branch on. Open: SS2–SS5 lanes add kinds additively as their methods land (Appendix B already registers them), and the spec's edge-case rule makes a new `kind` additive only because this domain is declared open. */ +export type ErrorKind = + | "parseError" + | "invalidRequest" + | "notInitialized" + | "alreadyInitialized" + | "methodNotFound" + | "invalidParams" + | "experimentalRequired" + | "internal" + | "pageEventTooLarge" + | "outputResultTooLarge" + | "overloaded" + | "inputTooLarge" + | "capabilityRequired" + | "notFound" + | "interrupted" + | "cancelled" + | "sessionNotFound" + | "sessionInUse" + | "sessionAmbiguous" + | "forkBoundaryInvalid" + | "sessionNotLoaded" + | "sessionStreamMismatch" + | "commandRejected" + | "backpressured" + | "skillNotFound" + | "viewTruncated" + | "outputUnavailable" + | "boundaryPruned" + | "boundaryUnusable" + | "noBoundary" + | "approvalNotFound" + | "approvalAlreadyResolved" + | "approvalChoiceInvalid" + | "approvalRequirementStale" + | "approvalReviewerUnavailable" + | "userInputNotFound" + | "userInputAlreadySettled" + | "userInputAnswerInvalid" + | (string & {}); + +/** The `error` member of an error response (SS1.2 §2.4). */ +export interface ErrorObject { + /** Registry code (Appendix B). Typed as a plain integer on the wire; the value domain is the registry below (INV-018). */ + code: number; + /** Structured detail; carries `kind` when present. */ + data?: ErrorData; + /** Human-readable message. Never a branch point (SS1.6). */ + message: string; +} + +/** An error response frame (SS1.2 §2.4). Exactly one of `result` or `error` appears on a response; this type is the `error` half. */ +export interface ErrorResponse { + /** The error payload (Appendix B registry). */ + error: ErrorObject; + /** Echo of the request id; `null` **only** for an unrecoverable parse error whose offending id could not be recovered (SS1.3). Always serialized, so `None` renders as `null` rather than being omitted — required-but-nullable (see [`require_nullable_id`]). */ + id: RequestId | null; + /** The literal `"2.0"`. */ + jsonrpc: JsonRpcVersion; +} + +/** Where a fork cuts the source history (tdd SS2.5.3). Turn ids are used instead of counts because ids stay stable across compaction and concurrent appends while indices do not. */ +export interface ForkCutPoint { + /** The last completed turn to copy, inclusive. Naming an in-progress or unknown turn fails `forkBoundaryInvalid`. */ + lastTurnId: string; +} + +/** Fork provenance folded from the durable `session.fork.created` record (`ForkProvenance`, tdd SS2.4). */ +export interface ForkProvenance { + /** The `commandId` of the `session/fork` that created it. */ + commandId: string; + /** An **opaque provenance string** preserved verbatim from the durable record. Display-only — not one of the SS6.2 cursor families: clients MUST NOT parse it and no method accepts it (tdd SS2.4). */ + cutCursor: string; + /** Whether the fork named an explicit cut point. */ + cutExplicit: boolean; + /** The source session this fork was cut from. */ + sessionId: string; +} + +/** The session goal block (tdd SS4.6.2, mirrors the runtime's goal state, camelCased). `status` and `percentComplete` are carried **verbatim** — out-of-contract status strings and >100 percents pass through; display clamping is the renderer's job. */ +export interface Goal { + /** Current work description, when present. */ + currentWork?: string; + /** Next work description, when present. */ + nextWork?: string; + /** The goal objective text. */ + objective: string; + /** Percent complete, verbatim (>100 passes through). */ + percentComplete: number; + /** Goal status, verbatim from the durable goal state (free string by design: out-of-contract values pass through). */ + status: string; +} + +/** `goal/clear` params (tdd SS3.18). An `objective` here is an unknown field and rejects `-32602 invalidParams` — the bare verbs carry exactly this pair. */ +export interface GoalClearParams { + /** The SS3.1.1 idempotency handle (UUIDv7). */ + commandId: string; + /** The target session. */ + sessionId: string; +} + +/** The shared SS3.18 goal ack, admission-only: all five verbs answer this one shape (the tdd's "Shared contract"), differing only in when `turnId` is present. */ +export interface GoalCommandResult { + /** Echoes the client's id. */ + commandId: string; + /** Admission status. */ + status: CommandStatus; + /** Present by exactly the four SS3.18 result cases: an idle wake carries the fresh goal turn's id; a busy `set`/`edit`/`resume` carries the admission-time active turn's id (a routing fact, not a delivery promise); `pause`/`clear` never carry it; an idle admission whose resulting goal is not unfinished parks the notification and carries none. **Additive-optional**: absent means "no turn named", never fabricated. */ + turnId?: string; +} + +/** `goal/edit` params (tdd SS3.18): replace the current goal's objective. Same shape as `goal/set`; the verbs differ in their `missing_goal` precondition, not their payload. */ +export interface GoalEditParams { + /** The SS3.1.1 idempotency handle (UUIDv7). */ + commandId: string; + /** The replacement objective; same trim/non-empty rule as `goal/set`. */ + objective: string; + /** The target session. */ + sessionId: string; +} + +/** `goal/pause` params (tdd SS3.18); shape as `goal/clear`. */ +export interface GoalPauseParams { + /** The SS3.1.1 idempotency handle (UUIDv7). */ + commandId: string; + /** The target session. */ + sessionId: string; +} + +/** `goal/resume` params (tdd SS3.18); shape as `goal/clear`. */ +export interface GoalResumeParams { + /** The SS3.1.1 idempotency handle (UUIDv7). */ + commandId: string; + /** The target session. */ + sessionId: string; +} + +/** `goal/set` params (tdd SS3.18): set the session's goal objective. */ +export interface GoalSetParams { + /** The SS3.1.1 idempotency handle (UUIDv7). */ + commandId: string; + /** The objective text; required, non-empty after Unicode-whitespace trim (the trimmed text is the payload). Empty-after-trim is `-32602 invalidParams` (tdd SS3.18). */ + objective: string; + /** The target session. */ + sessionId: string; +} + +/** What history a lifecycle result actually served (tdd SS2.5.2). Open on the client side: report-what-was-served means a client treats an unknown mode as "page it yourself". */ +export type HistoryMode = "anchoredSnapshot" | "inline" | "snapshot" | "none" | (string & {}); + +/** Why a result served `history.mode: "none"` — spec 208 FM-005's typed unavailability (tdd SS2.5.2; D-050). Open on the client side: an absent (older server) or unknown (newer server) value decodes conservatively as an unknown reason, and `viewCursor` text never substitutes for it (spec 208 INV-19734-1). */ +export type HistoryNoneReason = + | "excluded" + | "cursorSuffix" + | "historyBudget" + | "projectionUnavailable" + | "projectionReadLimit" + | (string & {}); + +/** The `history` request preference of `session/resume` (tdd SS2.5.2). The forced values downgrade `anchored` → `inline` → `snapshot` → `none`; under `auto` the full rung order is anchoredSnapshot → inline → snapshot → elided snapshot → none. */ +export type HistoryPreference = "auto" | "inline" | "snapshot" | "anchored"; + +/** Disposition when a turn is already running (tdd SS3.2). The wire default is `queue`: an SDK caller who has not looked at session state should not silently mutate an in-flight turn. */ +export type IfBusy = "queue" | "steer" | "replace"; + +/** `initialize` request params (SS1.4.1). */ +export interface InitializeParams { + /** Capability posture; absent means all defaults. */ + capabilities?: ClientCapabilities; + /** Client identification. */ + clientInfo: ClientInfo; +} + +/** The `initialize` result (SS1.4.1). */ +export interface InitializeResult { + /** Echo of the effective negotiated experimental setting. */ + experimentalApi: boolean; + /** Subset of the requested capabilities, plus policy-implicit grants. Fixed for the connection lifetime (INV-009). */ + grantedCapabilities: CapabilityName[]; + /** Absolute path of the muse home directory. */ + museHome: string; + /** Server runtime platform family. */ + platformFamily: PlatformFamily; + /** Server operating system. */ + platformOs: PlatformOs; + /** Envelope schema version and stable-surface fingerprint. */ + schema: SchemaInfo; + /** Server identification. */ + serverInfo: ServerInfo; + /** Whether this host persists its sessions (SS2.13). Optional so the addition is additive under SS1.5.4; a v1 host always sends it, and a client reads absent as `durable` because no server that omits it has the ephemeral profile. */ + sessionDurability?: SessionDurability; + /** Exact user-agent string presented to upstream model providers. */ + userAgent: string; +} + +/** One transcript item at one revision (tdd SS4.4.1 common fields plus the per-kind fields of SS4.5.2–4.5.10, all optional and owned by the kind their doc names). `item/started`, `item/updated`, and `item/completed` carry the full object; `item/delta` appends to it by field path. */ +export interface Item { + /** `subagent`: agent definition path. */ + agentPath?: string; + /** `toolCall`: the approval that gated this call (tdd SS5); join key for the approval view events. */ + approvalId?: string; + /** `toolCall`: the model-authored argument JSON, **verbatim**; clients parse. Verbatim keeps the fold byte-deterministic and survives model-emitted almost-JSON (tdd SS4.5.5). */ + args?: string; + /** `userMessage`: image attachment metadata only — base64 payloads are not echoed back on the view (tdd SS4.5.2). */ + attachments?: MessageAttachment[]; + /** `toolCall`: `true` once the task was durably backgrounded; delivered via `item/updated`. */ + background?: boolean; + /** `toolCall`: who backgrounded the task; absent on pre-split records — never inferred (tdd SS4.5.5). */ + backgroundInitiator?: BackgroundInitiator; + /** `toolCall`: the provider call id (`call_...`), opaque. */ + callId?: string; + /** `subagent`/`reminderChild`: the child's own session id, readable via `session/read`/`view/page` — child transcript drill-down without a second protocol (tdd SS4.5.7). */ + childSessionId?: string; + /** `reminderChild`: parent-session-dir-relative child log path; absent when no filesystem log. */ + childSessionLogPath?: string; + /** `workflow`: folded per-child state, keyed by `(childId, attempt)`, re-emitted whole on every change — the item `revision` is the ordering guard (tdd SS4.5.8). */ + children?: WorkflowChild[]; + /** `userMessage`/`userShell`: the submitting command — multi-client UIs de-duplicate their local echo on it. */ + commandId?: string; + /** `userShell`: the command text as submitted. */ + commandText?: string; + /** `subagent`: camelCased `SubagentControlStatus` (open enum); `status` stays the generic item vocabulary. */ + controlStatus?: SubagentControlStatus; + /** `subagent`: nesting depth. */ + depth?: number; + /** `userMessage`: presentation form (tdd SS3.2); absent when the client sent none. */ + displayText?: string; + /** `userShell`/`subagent`: observed wall-clock duration. */ + durationMs?: number; + /** `workflow`: launched entry identity. */ + entryId?: string; + /** `userShell`: the process exit code, when it exited by code. */ + exitCode?: number; + /** `userShell`: the terminating signal NUMBER, when signalled (e.g. 9) — the durable payload verbatim; nothing maps numbers to names, and the fold never invents one. */ + exitSignal?: number; + /** `toolCall`: machine-readable failure class (`TaskFailureKind`, snake_case verbatim — durable runtime vocabulary, the SS1.6 casing exemption). */ + failureKind?: string; + /** `toolCall`/`subagent`: `Failed`/`Rejected`/`Cancelled` reason text, verbatim. */ + failureReason?: string; + /** Server-provided one-line human summary any kind MAY carry, for generic rendering of kinds a client does not recognize; new kinds SHOULD carry it during their first release cycle (tdd SS4.10). */ + fallbackText?: string; + /** `reminderChild`: the reminder generation. */ + generationId?: number; + /** Bare UUIDv7; the item's identity across its whole lifecycle. Identity rule (tdd SS4.4.1): the task id for task-backed kinds (`toolCall`, `subagent`), the pre-minted commit `message_id` for delta-streamed kinds (`agentMessage`, `reasoning`), and the opening durable record's event id for everything else. Opaque to clients. */ + itemId: string; + /** The item kind (open enum): clients MUST render unknown kinds generically — kind name plus `status` plus `fallbackText` (tdd SS4.10). */ + kind: ItemKind; + /** `workflow`: the reconciled terminal message, set on completion. */ + message?: string; + /** `toolCall`: rich content the model saw beyond text; base64 is not inlined on the view (tdd SS4.5.5). */ + modelVisibleContent?: ModelVisibleContent[]; + /** `subagent`: objective as spawned. */ + objective?: string; + /** `compaction`: terminal only — folds installed/fallback status. */ + outcome?: CompactionOutcome; + /** `toolCall`/`userShell`: stored-output reference; fetch the full bytes via `item/readOutput` (served by #208). */ + outputRef?: OutputRef; + /** `toolCall`: stored structured-patch reference (`kind: "tool_patch"`, `mediaType: "application/json"`); the body is fetched via `item/readOutput` with `patchRef.id`, and the ref survives the SS2.5.2 elided-snapshot rung exactly like `outputRef` without ever being an elision trigger (tdd SS2.5.2, SS4.5.5; #33025). */ + patchRef?: OutputRef; + /** `toolCall`: server-authored edit-family diff summary (#33025) — always beside `patchRef`; absent = no diff available (tdd SS4.5.5). */ + patchSummary?: PatchSummary; + /** `reasoning`: provider reasoning item id (e.g. `rs_...`), for provider-side correlation. */ + providerItemId?: string; + /** `compaction`: noop/failure reason, verbatim (snake_case durable vocabulary, e.g. `"no_compactable_history"`). */ + reason?: string; + /** RFC3339 timestamp of the item's driving durable record. Absent on ephemeral-opened items until first durable re-emission (tdd SS4.4.1). */ + recordedAt?: string; + /** `reminderChild`: the reminder agent's id. */ + reminderAgentId?: string; + /** `subagent`: the result envelope from `ResultReady` (tdd SS4.5.7). */ + result?: SubagentResult; + /** `workflow`: set on resumed launches. */ + resumeFromRunId?: string; + /** `userMessage`: `true` after an accepted retract (tdd SS4.5.1); re-emitted via `item/updated`. */ + retracted?: boolean; + /** Integer >= 1, strictly monotonic per item; apply rule is replace-iff-higher. `item/delta` never bumps it (tdd SS4.4.1). */ + revision: number; + /** `subagent`: role as spawned. */ + role?: string; + /** `workflow`: launched script identity. */ + scriptId?: string; + /** Open enum; terminal = anything other than `"inProgress"`. Unknown values MUST be treated as terminal-unknown and rendered generically (tdd SS4.4.1). */ + status: ItemStatus; + /** `userMessage`: `true` when injected mid-turn via `turn/steer` or `ifBusy: "steer"`; absent otherwise. */ + steered?: boolean; + /** `compaction`: summarizer strategy (installed only). */ + strategyId?: string; + /** `subagent`: durable child identity (`subagent_id`). */ + subagentId?: string; + /** `compaction`: opaque provenance string naming the compaction boundary. A compaction-anchored read anchor since D-029 (2026-08-12): accepted by `session/resume` and `view/page` (wire shape and validation are the #208 lane's). Still opaque — clients relay it, never parse it (tdd SS4.5.10). */ + summarizedThrough?: string; + /** `reasoning`: one entry per summary part; part *n* streams via `item/delta` field `"summary.n"` (part boundary = index change). */ + summary?: string[]; + /** `reminderChild`: the linked task id. */ + taskId?: string; + /** `userMessage`: the prompt text as submitted. `agentMessage`: the accumulated reply, streamed via `item/delta` field `"text"`. `reasoning`: raw committed reasoning text where the provider exposes it (never streamed in v1, tdd SS4.5.4). */ + text?: string; + /** `compaction`: token budget snapshot after, when measured. */ + tokensAfter?: number; + /** `compaction`: token budget snapshot before, when measured. */ + tokensBefore?: number; + /** `toolCall`: tool name. */ + tool?: string; + /** `compaction`: what initiated the compaction. */ + trigger?: CompactionTrigger; + /** `workflow`: camelCased `WorkflowLaunchTriggerSource`, verbatim (durable runtime vocabulary, e.g. `"modelProposal"`). */ + triggerSource?: string; + /** `true` when the server's per-surface text budget saturated a streamed surface (`agentMessage.text`, `reasoning.summary[*]`, `toolCall.visibleOutput`, `userShell.visibleOutput`); the durable full text remains in the log (tdd SS4.5.4). */ + truncated?: boolean; + /** The owning turn (== the submitting `commandId` for fresh turns, tdd SS3.1.4). `null` only for `userShell` — the one kind outside a turn (tdd SS4.5.6). */ + turnId?: string | null; + /** `subagent`: **transitive** observed usage — the child and its own descendants; updated in place, absent until first observation; never folded into `session/tokenUsage.cumulative` (tdd SS4.6.5). */ + usage?: TokenUsage; + /** `toolCall`/`userShell`: bounded transcript-visible result text (budget per tdd SS4.5.4); streams via `item/delta` field `"output"`. */ + visibleOutput?: string; + /** `subagent`/`workflow`: the owning durable workflow run id (opaque string — not a UUID family). */ + workflowRunId?: string; +} + +/** `item/completed` params (tdd SS4.3): the item reached its terminal state — the authoritative final object. Always cites a durable `sourceRange` (tdd SS4.2). Clients MUST accept `item/completed` for an `itemId` they never saw `item/started` for (single-shot kinds; after a gap fill; tdd SS4.4.1). */ +export interface ItemCompletedParams { + /** The full item at its terminal revision. */ + item: Item; + /** The owning session. */ + sessionId: string; + /** The durable records this event folded from. */ + sourceRange: SourceRange; + /** Opaque, strictly monotonic view cursor (tdd SS4.1). */ + viewCursor: string; +} + +/** `item/delta` params (tdd SS4.3.1): a UTF-8-safe streaming append to an open item's field. Ephemeral-sourced by definition: this params object has **no** `sourceRange` member at all (spec 14653 INV-009 — the schema enforces the exemption). Deltas never bump `revision`. */ +export interface ItemDeltaParams { + /** The appended text; concatenation of a field path's deltas in cursor order equals that field's value on `item/completed` unless the surface saturated (spec 14653 INV-005). */ + delta: string; + /** Dotted field path the append targets (`"summary.0"`, `"output"`, ...); absent means `"text"` (tdd SS4.3.1). */ + field?: string; + /** The open item being appended to. */ + itemId: string; + /** The owning session. */ + sessionId: string; + /** Opaque, strictly monotonic view cursor (tdd SS4.1). */ + viewCursor: string; +} + +/** The nine v1 item kinds (tdd SS4.5.2–4.5.10). Open: a new kind is additive evolution, and clients MUST render unknown kinds generically (tdd SS4.10). */ +export type ItemKind = + | "userMessage" + | "agentMessage" + | "reasoning" + | "toolCall" + | "userShell" + | "subagent" + | "workflow" + | "reminderChild" + | "compaction" + | (string & {}); + +/** The `item/readOutput` content encoding (tdd SS4.7.4). Closed: text media is ALWAYS `utf8` and binary media `base64`; a third value would change the client's decode contract. */ +export type ItemReadOutputEncoding = "utf8" | "base64"; + +/** `item/readOutput` params (tdd SS4.7.4): byte-ranged fetch of stored full output that the view truncated — the `outputRef` fetch path SS3.9 and SS4.5.5 owe. Read-only; works on loaded and unloaded sessions; no lease. */ +export interface ItemReadOutputParams { + /** The item whose stored output is fetched. */ + itemId: string; + /** STORED bytes requested; default and max 6 MiB (tdd SS4.7.4). A value above the max is served at the max. */ + lengthBytes?: number; + /** STORED-byte offset to start from; 0 when omitted. STORED bytes are a property of the output, not of a transport (tdd SS4.7.4). */ + offsetBytes?: number; + /** The `outputRef.id` from the item (never the `uri`; the uri is display/provenance). */ + outputRef: string; + /** The session the item belongs to. */ + sessionId: string; +} + +/** `item/readOutput` result (tdd SS4.7.4): one stored-byte page. */ +export interface ItemReadOutputResult { + /** STORED bytes served in THIS response (not the output's total; that is `outputRef.byteLen` on the item). The next page's `offsetBytes` is `offsetBytes + byteLen` — short-serving and advancing are separate, so paging never skips or double-reads a byte (tdd SS4.7.4). */ + byteLen: number; + /** The served range, decoded per `encoding`. */ + content: string; + /** How `content` encodes the stored bytes. */ + encoding: ItemReadOutputEncoding; + /** `true` when this range reached the end of the stored output. */ + eof: boolean; + /** The stored output's media type (e.g. `text/plain`). */ + mediaType: string; + /** The STORED-byte offset this response actually served from. */ + offsetBytes: number; +} + +/** `item/started` params (tdd SS4.3): a new item opened on the transcript. `sourceRange` is absent when the item opens on an ephemeral record (the delta-streamed kinds); the item's authoritative open is re-stated by its durable-sourced `item/completed` (tdd SS4.2). */ +export interface ItemStartedParams { + /** The full item at revision 1. */ + item: Item; + /** The owning session. */ + sessionId: string; + /** The durable records this event folded from; absent on an ephemeral-sourced open (spec 14653 INV-009). */ + sourceRange?: SourceRange; + /** Opaque, strictly monotonic view cursor (tdd SS4.1). */ + viewCursor: string; +} + +/** Item status (tdd SS4.4.1). Open; terminal = anything other than `"inProgress"`, and unknown values are terminal-unknown, rendered generically. */ +export type ItemStatus = + | "inProgress" + | "completed" + | "failed" + | "cancelled" + | "rejected" + | "timedOut" + | (string & {}); + +/** `item/updated` params (tdd SS4.4.2): an open item changed non-terminally in a way deltas cannot express — the full item re-emitted at a higher revision (apply rule: replace iff higher). */ +export interface ItemUpdatedParams { + /** The full item at a higher revision. */ + item: Item; + /** The owning session. */ + sessionId: string; + /** The durable records this event folded from. */ + sourceRange: SourceRange; + /** Opaque, strictly monotonic view cursor (tdd SS4.1). */ + viewCursor: string; +} + +/** The literal `"2.0"` every frame carries (SS1.2). */ +export type JsonRpcVersion = "2.0"; + +/** `userMessage` image attachment metadata (tdd SS4.5.2): metadata only — the durable bytes live in the log and are reachable on the raw altitude. */ +export interface MessageAttachment { + /** Pixel height, when known. */ + height?: number; + /** The image media type (e.g. `"image/png"`). */ + mediaType: string; + /** Attachment type, `"image"` in v1 (free string: attachment vocabulary grows additively with the runtime's intake surface). */ + type: string; + /** Pixel width, when known. */ + width?: number; +} + +/** One visible catalog row (tdd SS3.10). Catalog rows marked hidden never reach the wire, so a client cannot select one. */ +export interface ModelCatalogEntry { + /** Context limit; `null` when the catalog source declared nothing. */ + contextLimit: number | null; + /** Cost block; `null` when the catalog source declared nothing. */ + cost: ModelCost | null; + /** The row's catalog default. Optional only for decoding older hosts; a present value is never null and is always a member of `variants`. Absent under `"unknown"` (ADR 33400 D2). */ + defaultReasoningEffort?: ReasoningEffort; + /** Description; `null` when the catalog source declared nothing. */ + description: string | null; + /** Presentation label. */ + displayLabel: string; + /** Marks the session's effective model when `sessionId` was supplied. May be false for every row — a client MUST NOT assume exactly one. */ + isActive: boolean; + /** Marks the catalog's default row. May be false for every row. */ + isDefault: boolean; + /** The selectable model id. */ + modelId: string; + /** Output limit; `null` when the catalog source declared nothing. */ + outputLimit: number | null; + /** Provider profile; `null` when the catalog source declared nothing. */ + profileId: string | null; + /** Provider routing. */ + providerId: string; + /** Described selectable tiers, a same-order subset of `variants`. Optional only for decoding older hosts; a present value is never null. Absent under `"unknown"` (ADR 33400 D2). */ + reasoningEffortVariants?: ModelDescribedReasoningEffort[]; + /** Release date; `null` when the catalog source declared nothing. */ + releaseDate: string | null; + /** Complete ordered selectable efforts. Optional only for decoding older hosts; a present value is never null. Current hosts always send it. */ + variants?: ModelReasoningEffortVariants; +} + +/** Where a model catalog came from (tdd SS3.10). **Open**: an unrecognized value is an unknown source, not an error. */ +export type ModelCatalogSource = + | "providerCatalog" + | "fakeCatalog" + | "unresolvedCatalog" + | "bundledCatalog" + | "configCatalog" + | (string & {}); + +/** What drove a model selection (tdd SS4.6.1). Open. */ +export type ModelChangeSource = "user" | "default" | "policy" | (string & {}); + +/** Per-1M-token catalog cost, carried **verbatim** for display and never rounded or re-formatted by the host (tdd SS3.10). Cost arithmetic stays client-local view math. */ +export interface ModelCost { + /** Cached-input cost, a decimal string. */ + cached: string; + /** Currency; nullable — today's catalogs are USD. */ + currency: string | null; + /** Input cost, a decimal string. */ + input: string; + /** Output cost, a decimal string. */ + output: string; +} + +/** One described reasoning-effort tier of a `model/list` row (tdd SS3.10, ADR 33400 D2): a selectable tier plus the catalog's display string for it when declared. */ +export interface ModelDescribedReasoningEffort { + /** The catalog's per-tier display string; absent when the catalog declared none. Optional only for decoding; a present value is never null. */ + description?: string; + /** The tier, in the closed D-024 vocabulary. */ + tier: ReasoningEffort; +} + +/** `model/list` params (tdd SS3.10): the discovery half of the model-picker gesture. **A query, not a command** — no `commandId`, no durable intake record, no view event. */ +export interface ModelListParams { + /** When present, the row matching that session's effective model is flagged `isActive`. When absent, no row is active — a client may list before it has a session (tdd SS3.10). */ + sessionId?: string; +} + +/** `model/list` result (tdd SS3.10). A snapshot at call time: v1 has no catalog subscription. `models` MAY be empty — a build shipped with no bundled models is a supported configuration. */ +export interface ModelListResult { + /** Visible rows only, newest `releaseDate` first. */ + models: ModelCatalogEntry[]; + /** The catalog's profile; `null` when none. */ + profileId: string | null; + /** The catalog's provider. */ + providerId: string; + /** Where the catalog came from — how a client tells a live provider catalog from a test fake and labels it honestly. */ + source: ModelCatalogSource; +} + +/** A complete ordered effort set, or an explicit unknown capability. */ +export type ModelReasoningEffortVariants = ReasoningEffort[] | UnknownModelReasoningEffortVariants; + +/** A model selection (tdd SS3.8). Empty strings are normalized to absent. */ +export interface ModelSelection { + /** Presentation label. */ + displayLabel?: string; + /** Catalog model id. Required. */ + modelId: string; + /** Provider profile. Omitted and explicit `null` both mean "no profile" (#23468). */ + profileId?: string | null; + /** Provider routing. */ + providerId?: string; +} + +/** Rich content the model saw beyond text (tdd SS4.5.5, `ModelVisibleToolContent`): metadata only — fetch bytes via `item/readOutput` when an `outputRef` exists. */ +export interface ModelVisibleContent { + /** Pixel height, when known. */ + height?: number; + /** The content media type. */ + mediaType: string; + /** The recorded content path. */ + path: string; + /** The tool that produced the content. */ + sourceToolName: string; + /** Content type, `"image"` in v1 (free string, grows with the durable vocabulary). */ + type: string; + /** Pixel width, when known. */ + width?: number; +} + +/** A notification frame, either direction (SS1.2 §2.2). No `id`; a notification is never answered. */ +export interface Notification { + /** Emission time in Unix milliseconds, recorded once at emission; server→client notifications only (SS1.2.5). Optional on the wire so decoders tolerate older servers. */ + emittedAtMs?: number; + /** The literal `"2.0"`. */ + jsonrpc: JsonRpcVersion; + /** Notification method name. */ + method: string; + /** Method-specific parameters; omitted entirely when empty. */ + params?: Record; +} + +/** Stored-output reference (tdd SS4.5.5): the camelCased fold of the durable `ToolOutputRef`. The fetch path is `item/readOutput` (#208). */ +export interface OutputRef { + /** Whether the bytes are servable (tdd SS4.5.5). */ + availability: OutputRefAvailability; + /** Stored byte length. */ + byteLen: number; + /** Content digest (e.g. `"sha256:..."`), when recorded. */ + digest?: string; + /** The stored output's durable id. */ + id: string; + /** The stored output's kind, verbatim durable vocabulary (e.g. `"tool_output"`; snake_case per the SS1.6 casing exemption). */ + kind: string; + /** Media type, when recorded. */ + mediaType?: string; + /** Stored path, when recorded. */ + path?: string; + /** The stored output's URI. */ + uri: string; +} + +/** Whether a stored output's bytes are servable (tdd SS4.5.5). Open — durable runtime vocabulary may grow additively. */ +export type OutputRefAvailability = + | "available" + | "missing" + | "unsupported" + | "accessFailed" + | (string & {}); + +/** Server-authored edit-family diff summary (tdd SS4.5.5, #33025): `files` counts the stored patch document's file entries; `added` and `removed` are the total `+`/`-` prefixed LINE counts summed over the stored patch's hunks across all files — line counts, never hunk or byte counts. Rides the `toolCall` item beside `patchRef`; the body is fetched via `item/readOutput` with `patchRef.id`. */ +export interface PatchSummary { + /** Total `+`-prefixed line count across all files' hunks. */ + added: number; + /** File entries in the stored patch document. */ + files: number; + /** Total `-`-prefixed line count across all files' hunks. */ + removed: number; +} + +/** A pending approval in the snapshot: the SS2.5.2 pointer shape plus the gated `itemId` when one exists (tdd SS4.9.1). */ +export interface PendingApprovalPointer { + /** The pending approval's id. */ + approvalId: string; + /** The parked item, when one exists. */ + itemId?: string; + /** The cursor the approval opened at. */ + viewCursor: string; +} + +/** The `pendingRequests` discriminator (tdd SS2.5.2). Open enum, v1 values `approval` and `userInput`. */ +export type PendingRequestKind = "approval" | "userInput" | (string & {}); + +/** A late-joiner pointer at an unsettled server-initiated request (tdd SS2.5.2, SS2.2.1). The full payloads arrive as re-issued server-to-client requests right after a `session/resume` response (tdd SS5.6); `session/read` never re-issues them (tdd SS2.5.5). **A second E4 instance, RULED with E4 (#22785, owner, 2026-08-26): the flat discriminated-object precedent is accepted for this type too, on the `ApprovalSubject` precedent, with the serialized JSON unchanged. A strict union node kind may be funded later; if it is, it must cover both this type and `TurnInputPart`.** SS2.5.2 ratifies two kind-discriminated entry shapes (`{kind: "approval", approvalId, viewCursor}` / `{kind: "userInput", userInputId, viewCursor}`); this is one flat object with both ids optional, so the published schema accepts `{"kind":"approval","userInputId":…}` and an entry carrying no id at all. The v1 model names no object-variant union (the `ApprovalSubject` precedent, followed for `TurnInputPart`), and this instance differs from that one in a way the ruling should see: `kind` here is an OPEN enum, where `TurnInputPart.type` is closed. */ +export interface PendingRequestPointer { + /** Present on an `approval` entry. */ + approvalId?: string; + /** The discriminator; v1 values `approval` and `userInput`. */ + kind: PendingRequestKind; + /** Present on a `userInput` entry. */ + userInputId?: string; + /** The cursor the request opened at. */ + viewCursor: string; +} + +/** A pending user-input prompt in the snapshot (tdd SS4.9.1, SS5.10). */ +export interface PendingUserInputPointer { + /** The parked item, when one exists. */ + itemId?: string; + /** The pending prompt's id. */ + userInputId: string; + /** The cursor the prompt opened at. */ + viewCursor: string; +} + +/** The server runtime's platform family (SS1.4.1); may differ from the client's. Closed: SS1.4.1 fixes the value set, so widening it is a deliberate, gate-visible protocol change rather than a silent addition (a later `Closed`→`Open` flip is itself additive, data-model.md §5.2). */ +export type PlatformFamily = "unix" | "windows"; + +/** The server's operating system (SS1.4.1). Closed, for the same reason as [`PlatformFamily`]. */ +export type PlatformOs = "macos" | "linux" | "windows"; + +/** The reasoning-effort tier sampled at submission (tdd SS3.2, SS3.3). The **same closed tier vocabulary** on both the fresh-turn and steer lanes, spelled identically; invalid tiers are invalid params. `none` is a tier of the vocabulary (ask for no reasoning), not a way to say "unset". */ +export type ReasoningEffort = + | "none" + | "minimal" + | "low" + | "medium" + | "high" + | "xhigh" + | "max" + | "ultra"; + +/** What drove a reasoning-effort default change (tdd SS4.6.9). Open; v1's only producer is `user` (an accepted `session/setReasoningEffort`). */ +export type ReasoningEffortChangeSource = "user" | "default" | "policy" | (string & {}); + +/** The snapshot's standing session-default reasoning effort (tdd SS4.9.1, ADR 31255 D1): the fold of the latest completed `runtime.reasoning_effort_reconfigure` fact — the same pair `session/reasoningEffortChanged` carries. */ +export interface ReasoningEffortState { + /** The standing default tier — the closed SS3.2 vocabulary, spelled identically to `turn/start.reasoningEffort`. */ + reasoningEffort: ReasoningEffort; + /** Which actor class set the default. */ + source: ReasoningEffortChangeSource; +} + +/** One end of a [`SourceRange`]: a record's event id and its sequence number within the stream (tdd SS4.2). */ +export interface RecordPosition { + /** The record's event id. */ + id: string; + /** The record's sequence number within its stream. */ + sequence: number; +} + +/** A request frame, either direction (SS1.2 §2.1). */ +export interface Request { + /** Client-chosen id, unique among that client's in-flight requests. */ + id: RequestId; + /** The literal `"2.0"`. */ + jsonrpc: JsonRpcVersion; + /** Slash-namespaced camelCase method name. */ + method: string; + /** Method-specific parameters; omitted entirely when empty, never `null`. */ + params?: Record; + /** W3C trace passthrough (SS1.8); requests only. */ + trace?: TraceContext; +} + +/** A request id (SS1.3): client-chosen string or integer. `1` and `"1"` do not compare equal; each direction owns its own id space. */ +export type RequestId = number | string; + +/** The SS5.3.3 presentation receipt — the client's response to a server-initiated request (`approval/request`, `userInput/request`). The response acknowledges presentation only ("a surface showed or will show this"): it changes no state, the server uses it for diagnostics alone, and the decision/answer travels as a command (`approval/decide`, `userInput/answer`). An error response or a dropped connection means this connection could not present the request; the approval or prompt stays pending and the request is re-issued on the next subscribe (SS5.6). There is no dismiss-without-deciding on the wire. */ +export interface RequestReceipt {} + +/** The `schema` pair in [`InitializeResult`] (SS1.4.1, SS1.5.3). `version` is the wire **envelope** schema version — distinct from the session-view `schemaVersion`, the raw-log `schema_version`, and the 0.x/1.0 stability posture, which is not on the wire at all (INV-006a). */ +export interface SchemaInfo { + /** `sha256:<64 lower hex>` content hash of this server binary's stable-surface bundle (INV-003). A mismatch is a warning condition, not an error (SS1.4.1). */ + fingerprint: string; + /** Envelope schema version; `1` in v1, expected to stay `1` for a long time (SS1.4.1). */ + version: number; +} + +/** Server identification inside [`InitializeResult`]. */ +export interface ServerInfo { + /** Server name. */ + name: string; + /** Server version string. */ + version: string; +} + +/** The session object every lifecycle method returns or lists (tdd SS2.4). Additive-optional evolution applies: clients must ignore unknown fields. */ +export interface Session { + /** The `turnId` of the current foreground turn when `status` is `running`, and `null` when the session is idle (tdd SS2.4). **Required-nullable, per the owner ruling on #22946** ("approve required-nullable", review comment `r3863946961`, 2026-08-26). Both production writers hard-code `"activeTurnId": null` (`session/start/commit_load.rs` for `session/start`, `session/snapshot.rs` feeding resume/read/list/fork) and nothing strips it, so this is what the binary emits and what SS2.4's example spells. The optional-absent shape published it as plain `string` and REJECTED every idle `Session` frame the host sent — the #19923 defect class this PR exists to close. E5's original evidence for optional-absent ("every committed transcript shows the omission") was wrong: those transcripts are hand-authored fixtures, not host recordings. They are corrected in the same change under the ruling's SS1.5 `regenerationJustification`. */ + activeTurnId: string | null; + /** The folded effective approval mode. **Additive-optional**: a Session object that omits it means the host has not folded a mode, which is why the index-derived `session/list` entry may legitimately omit it (tdd SS2.4, SS5.12). */ + approvalMode?: EffectiveApprovalModeState; + /** Attention flags (tdd SS2.4, ADR 31983 D3). **Additive-optional**, present only when at least one flag is set; absence is a NON-assertion (nothing pending, or the loaded session's pending source declined to answer). Loaded sessions only: a `notLoaded` row omits it even while its durable pending set is non-empty. */ + attention?: AttentionFlag[]; + /** Derived workspace branch (tdd SS2.14.1, #27598) — the index's last derived value; the live-change signal stays `session/branchChanged` (SS4.6.4). **Additive-optional**, omitted when underivable. */ + branch?: string; + /** RFC3339. For a fork this is the fork session id's UUIDv7 mint instant (tdd SS2.4). */ + createdAt: string; + /** Derived first-user-prompt preview (tdd SS2.14.1, #27598). **Additive-optional**, omitted when underivable. */ + firstUserPrompt?: string; + /** `null` for root sessions; fork provenance otherwise (tdd SS2.4). */ + forkedFrom: ForkProvenance | null; + /** Content-activity recency, RFC3339 (tdd SS2.4, ADR 31983 D5). **Additive-optional**: omitted when no content record exists. Never advanced by lifecycle bookkeeping (the resume marker, re-stamps) or a fork's copied seed-replay records, and never precedes `createdAt`. */ + lastActivityAt?: string; + /** The winning metadata fold's model; `null` when that record omits it. */ + modelId: string | null; + /** The durable allocated session name (tdd SS2.4/SS2.14.1, #27598; ADR 27598 D2/D4). **Additive-optional**: present when the serving path holds an allocated name, omitted otherwise — absent is never fabricated. Authoritative and renameable via `session/rename`. */ + name?: string; + /** Absolute path of the session's durable log; non-nullable. Under the ephemeral session profile it is the **empty string**, meaning "no durable log exists" (tdd SS2.13.2) — the one value a client must not hand to a filesystem call. */ + path: string; + /** The winning metadata fold's provider; `null` when that record omits it. */ + providerId: string | null; + /** The session identity. */ + sessionId: string; + /** Load state as this host knows it; `session/list` reports `notLoaded` for sessions loaded by *other* hosts (tdd SS2.4). */ + status: SessionStatus; + /** Derived display title (tdd SS2.14.1, #27598): a heuristic that may evolve — its carriage, not its derivation, is the contract. **Additive-optional**, omitted when underivable. */ + title?: string; + /** Completed-turn count from the session view fold (tdd SS2.4). */ + turnCount: number; + /** RFC3339; never precedes `createdAt` (tdd SS2.4). */ + updatedAt: string; + /** The winning metadata fold's workspace root; `null` when that record omits it — absent is never fabricated (tdd SS2.4). */ + workspaceRoot: string | null; +} + +/** `session/approvalModeChanged` params (tdd SS5.12): the fold of the durable approval-mode reconfigure audit fact — every accepted change writes one; a mode flip without an audit fact is a runtime bug by design. */ +export interface SessionApprovalModeChangedParams { + /** The requesting client's `clientInfo.name` (tdd SS1.4.1), recorded on the audit fact. */ + clientName: string; + /** The command that changed it. */ + commandId: string; + /** The mode now in effect. */ + mode: ApprovalMode; + /** The owning session. */ + sessionId: string; + /** How the mode took effect. */ + source: ApprovalModeSource; + /** The durable records this event folded from. */ + sourceRange: SourceRange; + /** Opaque, strictly monotonic view cursor (tdd SS4.1). */ + viewCursor: string; +} + +/** `session/branchChanged` params (tdd SS4.6.4): a durable workspace-branch observation landed. */ +export interface SessionBranchChangedParams { + /** The observed branch; `null` on a detached-HEAD observation — a fact, not "unchanged" (tdd SS4.6.4). */ + branch?: string; + /** The owning session. */ + sessionId: string; + /** The durable records this event folded from. */ + sourceRange: SourceRange; + /** The detected version-control system; absent when no repository was detected. */ + vcs?: Vcs; + /** Opaque, strictly monotonic view cursor (tdd SS4.1). */ + viewCursor: string; + /** The observed workspace root. */ + workspaceRoot: string; +} + +/** `session/closed` params (tdd SS2.6.2; enrolled by #33065): a loaded session was unloaded from this host, broadcast to every initialized connection. After it, the session is `notLoaded`; the log remains on disk and `session/resume` reloads it. A crash emits nothing — only the two orderly unload paths do, and both write the durable `SessionEnd` record first. */ +export interface SessionClosedParams { + /** Which orderly unload path fired. */ + reason: SessionClosedReason; + /** The unloaded session. */ + sessionId: string; + /** The final view head; a client that stores it can later `session/resume {cursor}` and receive exactly the (empty) suffix. An **opaque** view cursor: clients relay it, never parse it (tdd SS4.1). **Required-nullable**: the producers always write the member and write `null` when the final fold failed at unload (`session/idle.rs`, `serve/shutdown.rs`; PR #16779 review) — the host defines no cursor values of its own and never invents one, and the subscriber still learns the session closed. */ + viewCursor: string | null; +} + +/** The SS2.6.2 closed-reason vocabulary. Server-produced and published OPEN by owner ruling — the #33065 menu-B pick (PR #33201 comment 5638727249, recorded in the enrollment decision record), which settles that this notification reason enum publishes open rather than resting on E2's result-vocabulary rule by analogy. A crash is deliberately not a reason, because a crash emits no notification at all. */ +export type SessionClosedReason = "idle" | "hostShutdown" | (string & {}); + +/** `session/compact` params (tdd SS3.7): manually compact the session's conversation context — the `/compact` gesture. Compaction runs asynchronously; the ack is admission only. */ +export interface SessionCompactParams { + /** The SS3.1.1 idempotency handle (UUIDv7). */ + commandId: string; + /** The target session. */ + sessionId: string; + /** The run whose context to compact. Omit and the server resolves the session's current/latest run; a session with no resolvable run rejects with `commandRejected` reason `missing_run` (tdd SS3.7). Any syntactically valid UUID: a turn from an older session may carry a v4 or v5 id. A malformed id is `invalidParams`. */ + turnId?: string; +} + +/** `session/compact` result (tdd SS3.7). Failures *after* admission (`summarizer_failed`, `install_rejected`, `cancelled`) are not wire errors — they arrive as the compaction's terminal view event. */ +export interface SessionCompactResult { + /** Echoes the client's id. */ + commandId: string; + /** Why the command was a noop, e.g. `no_compactable_history`. Present on a `noop` status (tdd SS3.7). */ + reason?: string; + /** Admission status, or `noop`. */ + status: CompactStatus; +} + +/** The open configuration extension object shared by `session/start` and `session/resume` (ADR 32760 D1). Unknown keys are ignored on the wire, never rejected (tdd SS1.5.4, spec 206 INV-015; D-052 resolving [#23456](https://github.com/mslsrc/tbh/issues/23456)): the live decoder is D-042's tolerant path, and TEST-024 keeps every exported bundle free of `additionalProperties: false`. The Rust typed model's `deny_unknown_fields` is internal and never exported — the #22785 E6b closedness export applies nowhere, and the `x-msp-closed` opt-in mechanism stays inert. Recognized `mcpServers` values are typed and validated before they leave the wire boundary. */ +export interface SessionConfig { + /** Native MCP servers added only to this session runtime. */ + mcpServers?: Record; +} + +/** `session/contextUsage` params (tdd SS4.6.6, #14405): context-window pressure — the counted-once occupancy at the latest provider-reported durable fact, joined with the host's pressure basis. Replace wholesale; emitted only when the `(windowTokens, usedTokens, pressure)` triple changes value (identical adoptions emit nothing). */ +export interface SessionContextUsageParams { + /** Server-computed pressure level over the basis thresholds. */ + pressure: ContextPressureLevel; + /** The owning session. */ + sessionId: string; + /** The durable records this event folded from. */ + sourceRange: SourceRange; + /** Counted-once prompt tokens (#8803) plus output tokens of the driving record, saturating — the honest occupancy floor. */ + usedTokens: number; + /** Opaque, strictly monotonic view cursor (tdd SS4.1). */ + viewCursor: string; + /** The effective context-window size from the host's pressure basis; absent when the basis has no limit — the limit part is omitted, never invented (tdd SS4.6.6). */ + windowTokens?: number; +} + +/** The persisted deletion terminal, separate from admission (SS3.24). The shared schema is one object. Actual failed producers send both optional fields; completed producers omit both. Clients enforce those obligations before interpreting a known outcome. Preserve unknown strings without treating them as successful completion. */ +export interface SessionDeleteCompletedParams { + /** Idempotency key of the original deletion command. */ + commandId: string; + /** Deletion result; an unknown value leaves the command pending. */ + outcome: SessionDeleteOutcome; + /** Erasure evidence for a failed result; omitted for a completed result. */ + physicalChange?: SessionDeletePhysicalChange; + /** Required for a failed result and omitted for a completed result. */ + reason?: SessionDeleteFailureReason; + /** Session named by the original deletion command. */ + sessionId: string; +} + +/** Stable deletion-failure vocabulary known to this version (SS3.24). The server emits only these values. Future reason strings remain an open result vocabulary; consumers cannot infer completion from any reason. */ +export type SessionDeleteFailureReason = + | "ownershipUnavailable" + | "sharedSource" + | "writerBusy" + | "unsafeSource" + | "sourceChanged" + | "quiescenceFailed" + | "cancelled" + | "storageFailure" + | "cleanupIncomplete" + | "unsupportedLayout" + | (string & {}); + +/** Known terminal outcomes. A future outcome cannot authorize success or exit. */ +export type SessionDeleteOutcome = "completed" | "failed" | (string & {}); + +/** `session/delete` params. The host validates a non-nil legacy-valid UUID target and a UUID-v7 command identity before admission. Unknown members are ignored and do not enter the normalized command identity (SS1.5.4). */ +export interface SessionDeleteParams { + /** UUID-v7 idempotency key for this deletion command. */ + commandId: string; + /** Non-nil UUID of the session selected for deletion. */ + sessionId: string; +} + +/** The failed attempt's immutable snapshot of persisted physical evidence. `possible` is recorded before a detach/unlink, `confirmed` after a proved owned effect. A failure never resets that evidence merely because the session path is absent. Unknown strings remain representable; consumers must treat them conservatively as `possible`. */ +export type SessionDeletePhysicalChange = "none" | "possible" | "confirmed" | (string & {}); + +/** Whether this host writes its sessions to disk (SS1.4.1, SS2.13). A property of the host process, fixed at construction and identical for every session and every connection it serves — never requested, granted, or negotiated, which is why it is not a capability. Open: the unrepresented degraded state (#14401) has candidate resolutions that add a third value here, so closed would make that a breaking change. */ +export type SessionDurability = "durable" | "ephemeral" | (string & {}); + +/** `session/fork` params (tdd SS2.5.3). */ +export interface SessionForkParams { + /** The SS2.5 idempotency handle (UUIDv7). */ + commandId: string; + /** Copy history through a completed turn, inclusive. Omitted means "all completed turns" (tdd SS2.5.3). */ + cutPoint?: ForkCutPoint; + /** Skip inline history in the result and page it later — same as `session/resume` (tdd SS2.5.3). */ + excludeItems?: boolean; + /** The source session. */ + sessionId: string; +} + +/** `session/fork` result (tdd SS2.5.3): the `session/resume` envelope for the **new** session, whose `session.forkedFrom` carries the provenance. */ +export interface SessionForkResult { + /** The served history. */ + history: SessionHistory; + /** The late-joiner pointer set. */ + pendingRequests: PendingRequestPointer[]; + /** The new fork session, carrying `forkedFrom` provenance. */ + session: Session; + /** The new session's view head. */ + viewCursor: string; +} + +/** `session/goalChanged` params (tdd SS4.6.2): the projected goal block's value changed — identical adoptions emit nothing (the change gate), and an explicit `null` clears. Replace wholesale; `null` never means unchanged. */ +export interface SessionGoalChangedParams { + /** The full current goal block, or `null` to clear. */ + goal?: Goal; + /** The owning session. */ + sessionId: string; + /** The durable records this event folded from. */ + sourceRange: SourceRange; + /** Opaque, strictly monotonic view cursor (tdd SS4.1). */ + viewCursor: string; +} + +/** The `history` envelope shared by `session/resume`, `session/fork`, and `session/read` (tdd SS2.5.2). */ +export interface SessionHistory { + /** The full folded item array when `mode` is `inline`; `null` otherwise. */ + items: Item[] | null; + /** What was actually served — never what was asked for (tdd SS2.5.2). */ + mode: HistoryMode; + /** Why no history was served — sent exactly when `mode` is `"none"` (tdd SS2.5.2; D-050, spec 208 INV-19734-1). */ + noneReason?: HistoryNoneReason; + /** The folded view state when `mode` is `snapshot` or `anchoredSnapshot`; `null` otherwise. */ + snapshot: ViewSnapshot | null; +} + +/** `session/listChanged` params (#33084, ADR 33084 D3): one changed row of the `session/list` shape — the full SS2.4 Session object as the answering host's list face serves it at emission (the ADR 29243 live-overlaid row), a replace, never a delta. Emitted only on connections that negotiated the `sessionListStream` capability at `initialize` (ADR 33084 D2); row birth stays on `session/started`, unload pairs with `session/closed`. */ +export interface SessionListChangedParams { + /** The served list row for the changed session. */ + session: Session; +} + +/** `session/list` params (tdd SS2.5.4). Read-only; never touches leases. */ +export interface SessionListParams { + /** Opaque page cursor from a prior result. Page cursors are a distinct opaque-string family from view cursors and stream cursors, valid only for re-issuing the same listing (tdd SS2.5.4). Omitted and explicit `null` both mean "first page" (#23468). */ + cursor?: string | null; + /** Page size; default 50, max 200 — the cap is published under #22785 E6c (owner-ruled 2026-08-26). Maximum 200. */ + limit?: number; + /** Only sessions with log activity after this RFC3339 instant. */ + updatedAfter?: string; + /** Only sessions whose metadata workspace root equals this path. */ + workspaceRoot?: string; +} + +/** `session/list` result (tdd SS2.5.4). Ordering is `updatedAt` descending. */ +export interface SessionListResult { + /** The next page's cursor; `null` on the last page. */ + nextCursor: string | null; + /** The page of stored sessions. */ + sessions: Session[]; +} + +/** One native MCP server supplied at session construction (ADR 32760 D1). **Closed union** (#33295 owner ruling A1, extending D-033/D-050 to `oneOf`): a validator MUST reject an undeclared `transport` arm — the server fails `session/start` decode on an unknown transport, and a future transport arrives as an explicit schema addition. */ +export type SessionMcpServerConfig = + | { + /** Ordered child arguments. */ args?: string[]; + /** Executable name or path. Configuration diagnostics never echo it. */ command: string; + /** Explicit child environment additions. */ env?: Record; + /** Stdio framing mode; defaults to automatic detection. */ framing?: SessionMcpStdioFraming; + /** Required/optional startup posture; defaults to required. */ mode?: SessionMcpServerMode; + transport: "stdio"; + } + | { + /** Explicit request headers. */ headers?: Record; + /** Required/optional startup posture; defaults to required. */ mode?: SessionMcpServerMode; + transport: "streamableHttp"; + /** HTTP(S) endpoint. Configuration diagnostics never echo it. */ url: string; + }; + +/** Startup failure posture for a session MCP server. */ +export type SessionMcpServerMode = "required" | "optional"; + +/** Stdio framing choices exposed by session MCP configuration. */ +export type SessionMcpStdioFraming = "auto" | "contentLength" | "lineDelimitedJson"; + +/** `session/modelChanged` params (tdd SS4.6.1): a durable model-selection record landed (tdd SS3.8). */ +export interface SessionModelChangedParams { + /** The selected model. */ + modelId: string; + /** The selected provider; `null` when the selection names none. */ + providerId?: string; + /** The owning session. */ + sessionId: string; + /** What drove the selection. */ + source: ModelChangeSource; + /** The durable records this event folded from. */ + sourceRange: SourceRange; + /** Opaque, strictly monotonic view cursor (tdd SS4.1). */ + viewCursor: string; +} + +/** `session/modelRouteUnserved` params (#25603, ADR 25603 D1, spec 5469 Amendment 16): an accepted `login.credential_update` installed a provider that cannot serve the session's STANDING model route. Disclosure only — the standing selection is unchanged (no `session/modelChanged` fires) and the repair path is a routable `session/setModel`. Folded from the durable `runtime.provider_reconfigure.standing_route_unserved` record; additive — clients that predate it skip the unknown method. */ +export interface SessionModelRouteUnservedParams { + /** The command whose accepted swap installed the provider. */ + commandId: string; + /** The provider id the swap installed. */ + installedProviderId: string; + /** The standing (still bound, now unroutable) model. */ + modelId: string; + /** The standing model's provider; `null` when the selection names none. */ + providerId?: string; + /** The owning session. */ + sessionId: string; + /** The durable records this event folded from. */ + sourceRange: SourceRange; + /** Opaque, strictly monotonic view cursor (tdd SS4.1). */ + viewCursor: string; +} + +/** `session/nameChanged` params (tdd SS4.6.7, #27598): the fold of ANY durable session-name record — the FIRST-NAMING automatic allocate as well as an accepted `session/rename` (or the same in-process runtime command). Replace-wholesale like every SS4.6 sibling. */ +export interface SessionNameChangedParams { + /** The new canonical name now in effect. */ + name: string; + /** The owning session. */ + sessionId: string; + /** The durable records this event folded from. */ + sourceRange: SourceRange; + /** Opaque, strictly monotonic view cursor (tdd SS4.1). */ + viewCursor: string; +} + +/** `session/read` params (tdd SS2.5.5): read one stored session **without attaching** — no writer lease, no load, no subscription, no `SessionResumed` record. */ +export interface SessionReadParams { + /** Metadata-only unless you ask: `false` carries the folded item history. Same name and polarity as `session/resume`/`session/fork`; only the default differs — here it is `true` (tdd SS2.5.5). */ + excludeItems?: boolean; + /** The session to read. */ + sessionId: string; +} + +/** `session/read` result (tdd SS2.5.5). The `viewCursor` is the fold head at read time — a point-in-time read, immediately stale if a foreign host is appending. */ +export interface SessionReadResult { + /** The served history. */ + history: SessionHistory; + /** The same pointer shape as `session/resume`. Because `session/read` never subscribes this is a point-in-time log read only: no requests are re-issued after it (tdd SS2.5.5). */ + pendingRequests: PendingRequestPointer[]; + /** The session as folded point-in-time. */ + session: Session; + /** The fold head at read time. */ + viewCursor: string; +} + +/** `session/reasoningEffortChanged` params (tdd SS4.6.9, ADR 31255 D1): a durable reasoning-effort reconfigure record landed (tdd SS3.21). Replace wholesale, latest wins; the snapshot carries the latest value (SS4.9.1). */ +export interface SessionReasoningEffortChangedParams { + /** The new session default: the closed eight-string tier vocabulary of tdd SS3.2, spelled identically. */ + reasoningEffort: ReasoningEffort; + /** The owning session. */ + sessionId: string; + /** What drove the change. */ + source: ReasoningEffortChangeSource; + /** The durable records this event folded from. */ + sourceRange: SourceRange; + /** Opaque, strictly monotonic view cursor (tdd SS4.1). */ + viewCursor: string; +} + +/** `session/rename` params (tdd SS2.14.2, #27598): set or change the durable allocated session name through the runtime `session_name` command. One writer; withheld under the ephemeral profile. */ +export interface SessionRenameParams { + /** The SS3.1.1 idempotency handle (UUIDv7). */ + commandId: string; + /** The requested name; the runtime's normalization and validation apply. */ + name: string; + /** The target session. */ + sessionId: string; +} + +/** `session/rename` result (tdd SS2.14.2, #27598): `name` is the canonical (normalized) name the durable record settled. */ +export interface SessionRenameResult { + /** Echoes the client's id. */ + commandId: string; + /** The settled canonical name. Omitted on the `RecoveryPending` arm, which answers `{commandId, status: "accepted"}` and delivers the name via `session/nameChanged` / `Session.name` (tdd SS2.14.2). */ + name?: string; + /** Admission status. */ + status: CommandStatus; +} + +/** `session/resume` params (tdd SS2.5.2). */ +export interface SessionResumeParams { + /** The SS2.5 idempotency handle (UUIDv7). A resume that loads a session writes a durable `SessionResumed` record. */ + commandId: string; + /** The same open construction-time extension object as `session/start`. */ + config?: SessionConfig; + /** A view cursor previously observed by this client — or an observed `summarizedThrough` compaction anchor (tdd SS4.5.10, D-029). When present the server returns only the suffix and `history.mode` is `none` (tdd SS2.5.2). Omitted and explicit `null` both mean "no cursor" (#23468). */ + cursor?: string | null; + /** Return only session metadata and live resume state; page history separately with `view/page`. Default `false` (tdd SS2.5.2). */ + excludeItems?: boolean; + /** History mode preference, default `auto`. A PREFERENCE, not a budget override: when the requested mode does not fit the history budget the server downgrades it and `history.mode` reports what was actually served (tdd SS2.5.2). Ignored when `cursor` or `excludeItems` makes it moot. */ + history?: HistoryPreference; + /** The session to load. */ + sessionId: string; +} + +/** `session/resume` result (tdd SS2.5.2). */ +export interface SessionResumeResult { + /** The served history. */ + history: SessionHistory; + /** The late-joiner pointer set; empty when nothing is pending. */ + pendingRequests: PendingRequestPointer[]; + /** The loaded session. */ + session: Session; + /** The session view head; the connection is subscribed after it. A still-loaded session a prior `view/unsubscribe` detached is RE-ATTACHED by this resume (spec 208 FR-002a, #28920): live delivery resumes from this head. Explicit-cursor re-attach with `(after, head]` engine replay stays `view/subscribe` (SS4.7.1). */ + viewCursor: string; +} + +/** `session/setApprovalMode` params (tdd SS5.12): switch the session's approval enforcement mode mid-session. A standard SS3 command. **Select, never create.** A client may *select* a mode the host's configuration already defines; it may not construct one, supply inline rules, or otherwise describe a policy on the wire — which is why [`ApprovalMode`] is closed. */ +export interface SessionSetApprovalModeParams { + /** The SS3.1.1 idempotency handle (UUIDv7). */ + commandId: string; + /** The preconfigured mode to select. The requesting actor is recorded from the connection's `clientInfo.name` (tdd SS1.4.1). */ + mode: ApprovalMode; + /** The target session. */ + sessionId: string; +} + +/** `session/setApprovalMode` result (tdd SS5.12). Applies next-action — an in-flight tool action's pending approval is not retroactively decided by a mode change. */ +export interface SessionSetApprovalModeResult { + /** Whether the change did anything. */ + applyOutcome: ApprovalModeApplyOutcome; + /** Echoes the client's id. */ + commandId: string; + /** The folded effective-mode projection — the same object the `Session` carries as `approvalMode` (tdd SS2.4, SS5.12). */ + effectiveMode: EffectiveApprovalModeState; + /** Admission status. */ + status: CommandStatus; +} + +/** `session/setModel` params (tdd SS3.8): the model-picker gesture. The selection is durable and applies to subsequent model calls. */ +export interface SessionSetModelParams { + /** The SS3.1.1 idempotency handle (UUIDv7). */ + commandId: string; + /** The selection. */ + model: ModelSelection; + /** The target session. */ + sessionId: string; +} + +/** `session/setModel` result (tdd SS3.8). If a turn is running the selection is admitted now and applied at the next model-call boundary; the ack does not wait for that boundary. */ +export interface SessionSetModelResult { + /** Echoes the client's id. */ + commandId: string; + /** Admission status. */ + status: CommandStatus; +} + +/** `session/setReasoningEffort` params (tdd SS3.21, ADR 31255 D1): set the session's standing reasoning-effort default. A turn carrying its own `reasoningEffort` overrides it for that turn only; the default overrides the host's configured default. Same accept/reject and `commandId` idempotency shape as `session/setModel` (SS3.8). */ +export interface SessionSetReasoningEffortParams { + /** The SS3.1.1 idempotency handle (UUIDv7). */ + commandId: string; + /** The new session default: the closed eight-string tier vocabulary `turn/start` carries (SS3.2), spelled identically. Invalid tiers are invalid params. A default of `max` rides provider requests verbatim, never clamped (ADR 28621 D8, ADR 31131 D1). */ + reasoningEffort: ReasoningEffort; + /** The target session. */ + sessionId: string; +} + +/** `session/setReasoningEffort` result (tdd SS3.21). The default is durable on ack and applies to turns launched after admission; a running turn keeps the request options it already resolved. */ +export interface SessionSetReasoningEffortResult { + /** Echoes the client's id. */ + commandId: string; + /** Admission status. */ + status: CommandStatus; +} + +/** `session/start` params (tdd SS2.5.1). */ +export interface SessionStartParams { + /** The session's starting approval mode; server default when omitted or explicit `null` — both spellings select the default (#23468). Select, never create — the value names a mode the host's configuration already defines (tdd SS2.5.1, SS5.12). This is the only surface that declares a non-interactive run's policy (tdd SS5.11, D-008). */ + approvalMode?: ApprovalMode | null; + /** The SS2.5 idempotency handle (UUIDv7). Required; the server never mints one. */ + commandId: string; + /** Reserved for per-session overrides owned by a future Configuration section. Issue #32760 admits only its `mcpServers` member. */ + config?: SessionConfig; + /** Initial model; server default when omitted. */ + modelId?: string; + /** Initial provider routing; server default when omitted or explicit `null` — both spellings select the default (#23468). */ + providerId?: string | null; + /** Exact identity for this new root session. When omitted the server mints a UUIDv7. This field never selects an existing session: a retained or reserved id is rejected `commandRejected` with reason `session_id_conflict` (tdd SS2.5.1). */ + sessionId?: string; + /** Absolute path, folded into the first metadata record. */ + workspaceRoot?: string; + /** The session's initial runtime workspace-root set: ordered absolute canonical existing directory paths; the first root is the CWD and plays the primary-root role; every root receives the ADR 11237 `:workspace_roots` rules; extra roots grant no Project Trust. When `workspaceRoot` is also present the two must name the same folder, compared on canonical forms (the raw `workspaceRoot` is canonicalized first; ADR 37313 D9), or the request is invalid params, and so is explicit `null` (the D-049 null-tolerant set is closed and excludes this member). Omitted preserves single-root behavior (tdd SS2.5.1, ADR 37313, #37313). */ + workspaceRoots?: string[]; +} + +/** `session/start` result (tdd SS2.5.1). */ +export interface SessionStartResult { + /** The new session. */ + session: Session; + /** The session view head after the start fold; the connection is subscribed and receives every view event after this cursor. A deduplicated retry returns this same result (tdd SS2.5.1). */ + viewCursor: string; +} + +/** `session/started` params (tdd SS2.6.1; enrolled by #33065): a session became newly loaded on this host via `session/start` or `session/fork` (never `session/resume` — Appendix C, OQ-F), broadcast to every initialized connection. The one member is the same `$defs/Session` object the `session/start`/`session/fork` results carry, so the broadcast and the result describe one fact through one type — the host builds the broadcast payload as a `$defs/Session` — the fresh-start arm reuses the result's `session` member, the start-replay arm builds the live attach snapshot — and the emission is parity-gated against this type at the producer (`session-server` prod-assembly capture) and over the committed transcript corpus (conformance `session_lifecycle_enrollment`). */ +export interface SessionStartedParams { + /** The newly loaded session, as of the start/fork fold. */ + session: Session; +} + +/** A session's load state (tdd SS2.4). */ +export type SessionStatus = "notLoaded" | "idle" | "running" | (string & {}); + +/** `session/statusChanged` params (tdd SS4.6.10, ADR 31983 D2): a loaded session's projected `(status, attention)` value flipped. A command-plane broadcast — delivered to every connection regardless of its view subscription set, never gated, no `sourceRange`; the same facts live on the Session object, which is how a client seeds its table (no initial burst on connect). */ +export interface SessionStatusChangedParams { + /** Attention flags after the transition; present only when at least one flag is set (tdd SS2.4, ADR 31983 D3). */ + attention?: AttentionFlag[]; + /** The owning session. */ + sessionId: string; + /** Load state after the transition (the closed SS2.4 enum, unchanged). */ + status: SessionStatus; + /** The causing record's view cursor (ADR 31983 D4). Required-nullable: `null` exactly when the paired `session/closed`'s cursor is `null` (the unload fold-failure arm); a definite cursor everywhere else. */ + viewCursor: string | null; +} + +/** `session/todoListChanged` params (tdd SS4.6.3): a `TodoSnapshotUpdated` record landed. Replace the whole list on every event; an empty `items` array is a cleared list, not a no-op. */ +export interface SessionTodoListChangedParams { + /** The full todo list, replaced wholesale. */ + items: TodoItem[]; + /** The snapshot revision. Diagnostics only, never an ordering guard — `viewCursor` orders (tdd SS4.6.3). */ + revision: number; + /** The owning session. */ + sessionId: string; + /** The durable records this event folded from. */ + sourceRange: SourceRange; + /** The tool that produced the snapshot, verbatim. */ + sourceTool: string; + /** Opaque, strictly monotonic view cursor (tdd SS4.1). */ + viewCursor: string; +} + +/** `session/tokenUsage` params (tdd SS4.6.5): one per model completion that reports usage. Carries the raw counters verbatim plus the server-derived counted-once `promptTokens`/`totalTokens` (#8803: clients display and sum these and never re-derive the provider's cache convention) and the session `cumulative` block. Accumulate-only: `cumulative` never goes backward. */ +export interface SessionTokenUsageParams { + /** Session running totals at this cursor; the fold owns the accumulation. Subagent/workflow-child usage is never folded in — it rides the owning items (tdd SS4.6.5). */ + cumulative: CumulativeTokenUsage; + /** Model-call wall time, when measured. */ + durationMs?: number; + /** Provider finish reason when reported (open vocabulary, verbatim). */ + finishReason?: string; + /** The effective model that produced this usage; `null` on pre-schema records — never back-filled, an unpriced leg (tdd SS4.6.5). */ + modelId?: string; + /** Prompt tokens counted exactly once under the provider's cache convention (#8803). Server-derived; deterministic — the provider is in the log. */ + promptTokens: number; + /** The owning session. */ + sessionId: string; + /** The durable records this event folded from. */ + sourceRange: SourceRange; + /** `promptTokens + outputTokens` — the honest per-completion total. */ + totalTokens: number; + /** The turn whose model call completed. */ + turnId: string; + /** Raw counters verbatim from the durable record. */ + usage: TokenUsage; + /** Opaque, strictly monotonic view cursor (tdd SS4.1). */ + viewCursor: string; +} + +/** `session/userShell` params (tdd SS3.9): run a user-initiated shell command in the session's workspace — the TUI's `!` escape hatch. **Capability-gated**: the connection must negotiate `userShell` at `initialize`, and the whole justification is the SS1.10 spawn boundary. */ +export interface SessionUserShellParams { + /** The SS3.1.1 idempotency handle (UUIDv7). */ + commandId: string; + /** The shell command to run. */ + commandText: string; + /** The target session. */ + sessionId: string; +} + +/** `session/userShell` result (tdd SS3.9): immediate. The shell runs off the command worker so a long command cannot stall the command plane; the output arrives as the `userShell` item's terminal view event. */ +export interface SessionUserShellResult { + /** Echoes the client's id. */ + commandId: string; + /** Admission status. */ + status: CommandStatus; +} + +/** The view-health state (tdd SS2.5.2 companion). Open enum (`x-msp-openness: open`): a client MUST ignore an unrecognized value. v1 emits only `Unavailable`; openness itself reserves additive room for a future re-arm (ADR 32557 D1 names it `healthy`) and for the #14401 durable-degraded axis, so no never-emitted token is baked onto the stable surface now. */ +export type SessionViewHealth = "unavailable" | (string & {}); + +/** `session/viewHealthChanged` params (ADR 32557; #32557): the named session's live view stream became unavailable, and why. Best-effort: ordered after already-queued view frames and may be dropped (for example when the connection is closing), so a client MUST NOT assume a guaranteed push and still reads `history.noneReason` on its next resume/read (FM-005). */ +export interface SessionViewHealthChangedParams { + /** The new view-health state. In v1 the host emits only `Unavailable`. */ + health: SessionViewHealth; + /** Why the view is unavailable — the SAME typed `HistoryNoneReason` vocabulary the pull path stamps on `history.noneReason` (tdd SS2.5.2, D-050), so a client reuses one handler for push and pull. `projectionUnavailable` in v1; present only when `health` is `Unavailable`. Open on the client side: an unknown reason decodes conservatively and renders generically. */ + noneReason?: HistoryNoneReason; + /** The session whose live view health changed. */ + sessionId: string; +} + +/** One typed-invocable shortcut spelling (tdd SS3.22.1). A plugin skill may contribute two rows: its bare-name winner and its qualified `:` form. */ +export interface SkillCatalogEntry { + /** Present when the skill declares an argument hint. */ + argumentHint?: string; + /** The skill's palette summary. */ + description: string; + /** The skill display name the first-party palette shows. */ + displayName: string; + /** The owning plugin's identifier; present when `source` is `plugin`. */ + pluginId?: string; + /** The shortcut token without the leading slash — bare (`fix-bug`) or plugin-qualified (`acme:deploy`) — the exact value a `skill` input part submits. Unique within one response. */ + selector: string; + /** Where the skill comes from. */ + source: SkillSource; +} + +/** `skill/changed` params (tdd SS3.22.2): the session's user-invocable set changed; clients re-issue `skill/list`. Advisory — the host may coalesce bursts and guarantees no ordering relative to view events (ADR 32471 D3). */ +export interface SkillChangedParams { + /** The session whose set changed. */ + sessionId: string; +} + +/** `skill/list` params (tdd SS3.22.1). Per-session because skill scope follows the session's workspace and plugin state. */ +export interface SkillListParams { + /** The target session. */ + sessionId: string; +} + +/** `skill/list` result (tdd SS3.22.1): one row per typed-invocable shortcut spelling — exactly the invocations the first-party typed dispatch accepts (spec 11352 INV-007 predicate and the shared shortcut layer's name resolution, both by call-through; ADR 32471 D2). */ +export interface SkillListResult { + /** The session's user-invocable skill rows. */ + skills: SkillCatalogEntry[]; +} + +/** A skill row's source scope (tdd SS3.22.1): the projection of the skills crate's `SkillsSourceScope`. Open (server-produced result vocabulary, the #22785 enum-openness rule): a future scope value is additive. */ +export type SkillSource = "bundled" | "user" | "project" | "plugin" | (string & {}); + +/** The compaction boundary an anchored snapshot is anchored at (tdd SS2.5.2). */ +export interface SnapshotAnchor { + /** The view cursor of the boundary's `compaction` event. */ + boundaryCursor: string; + /** The boundary's opaque compaction anchor (tdd SS4.5.10). */ + summarizedThrough: string; +} + +/** The complete folded view at the snapshot cursor (tdd SS4.9.1). Additive-optional evolution applies exactly as everywhere else; unknown members MUST be ignored, and preserved if re-serialized. **One served site does not satisfy this type today, escalated under #22785 (E8).** The genesis snapshot rung serves `state: {"items": [...]}` alone (`tbh-session-view-serve`'s `materialized_fold.rs`, the `select_genesis_history` call), while the anchored rung serves the full folded state. A schema-validating client resuming with `{"history": "snapshot"}` against the real fold therefore sees the other required members missing. The fix is a serving change in the #208/#14653 lane (serialize the `SessionViewState` the genesis path already folds, as the anchored path does) plus its own RED against the real fold; it is not a shape change here, because SS4.9.1 ratifies this shape. */ +export interface SnapshotState { + /** The running foreground turn; `null` when none is running. */ + activeTurn: TurnRef | null; + /** The effective approval mode at this cursor — the same object the `Session` carries (tdd SS2.4, SS5.12). */ + approvalMode: EffectiveApprovalModeState; + /** Latest branch observation; `null` when no fact has landed. */ + branch: BranchState | null; + /** The latest `(windowTokens, usedTokens, pressure)` triple. SS4.9.1 says "`null`/absent" and SS4.9.2's worked example OMITS the member; the binary omits it too, so this is the absent arm — the four siblings above are present-null in the same frame. INV-017 admits one or the other, never both; escalated under #22785. */ + contextUsage?: ContextUsage; + /** Latest effective model; `null` when no fact has landed. */ + effectiveModel: EffectiveModel | null; + /** Latest goal block; `null` when no fact has landed. */ + goal: Goal | null; + /** Every item, in first-opened order, each at its latest revision at the snapshot cursor. One schema per kind, shared with the notification item object — which is what makes snapshot+suffix a pure splice (tdd SS4.9.1). */ + items: Item[]; + /** The settled canonical session name; `null` when the session was never named (#27598, tdd SS4.9.1 / SS2.14). Folds the same durable `session/nameChanged` record `Session.name` reports. */ + name?: string; + /** The pending-approval half of the pending set. */ + pendingApprovals: PendingApprovalPointer[]; + /** The pending-user-input half of the pending set. */ + pendingUserInputs: PendingUserInputPointer[]; + /** Admitted-but-not-launched submits, in launch order (tdd SS3.1.4). */ + queuedTurns: TurnRef[]; + /** The latest session-default reasoning effort with its source; ABSENT until a set lands (tdd SS4.9.1 `reasoningEffort`, ADR 31255 D1 — the `contextUsage` absent arm, never present-null). */ + reasoningEffort?: ReasoningEffortState; + /** Latest todo list; `null` when no fact has landed. */ + todoList: TodoListState | null; + /** The `cumulative` block of the last `session/tokenUsage` event; zeroes when none (tdd SS4.9.1). */ + tokenUsage: CumulativeTokenUsage; + /** Completed-turn count at this cursor. */ + turnCount: number; +} + +/** The inclusive raw-record range a durable-sourced view event folded from (tdd SS4.2) — the reconciliation token: re-folding the cited records from the canonical fold state immediately before `first` — appended host-death terminals seed from the end-of-records state — reproduces the event (tdd SS7.5; spec 14653 T041/INV-009). In v1 it is an **opaque provenance token**: no wire method reads what it points at (the SS6 raw-log altitude is deferred, #13929). Ephemeral-sourced events (`item/delta`, and an `item/started` that opens on an ephemeral record) carry no `sourceRange` — ephemeral records may be evicted by retention (tdd SS4.2; spec 14653 INV-009). */ +export interface SourceRange { + /** The first record of the inclusive range. */ + first: RecordPosition; + /** The last record of the inclusive range; equal to `first` for an event folded from one record. */ + last: RecordPosition; + /** The stream the folded records live on (run stream, or the session stream's mirror of task/approval/subagent-control records — whichever the fold actually consumed, tdd SS4.2). */ + stream: StreamRef; +} + +/** One raw stream named by a [`SourceRange`] (tdd SS4.2). */ +export interface StreamRef { + /** The stream id. */ + id: string; + /** The stream kind, verbatim raw-log vocabulary (e.g. `"run"`, `"session"`). A free string, not an enum: `sourceRange` is an opaque provenance token in v1 and the raw stream vocabulary is the deferred SS6 altitude's to freeze (#13929). */ + kind: string; +} + +/** Subagent control status (tdd SS4.5.7, camelCased `SubagentControlStatus`). Open; the generic item `status` is the terminal authority. */ +export type SubagentControlStatus = + | "accepted" + | "starting" + | "running" + | "resultReady" + | "closing" + | "closed" + | "recoveryPending" + | "manualReconciliation" + | (string & {}); + +/** Params for `subagent/sendMessage` and `subagent/followupTask` (SS3.16): a child target plus the input body. */ +export interface SubagentInputParams { + /** The note/task text: trimmed with Unicode-whitespace semantics, rejected when empty after trimming, delivered trimmed with multibyte content byte-for-byte intact (SS3.16). */ + body: string; + /** Client-minted UUIDv7 command id (SS3.1.1). */ + commandId: string; + /** Target session (UUIDv7 string; SS3 preamble). */ + sessionId: string; + /** Durable child id (the SS4.5.7 item's `subagentId`; opaque string). */ + subagentId: string; +} + +/** Params for `subagent/interrupt`, `subagent/stop`, and `subagent/close` (SS3.16): a child target plus an optional human-readable reason that is preserved on the durable effect record. */ +export interface SubagentOwnerReasonParams { + /** Client-minted UUIDv7 command id (SS3.1.1). */ + commandId: string; + /** Optional reason, preserved on the durable owner-command record. */ + reason?: string; + /** Target session (UUIDv7 string; SS3 preamble). */ + sessionId: string; + /** Durable child id (the SS4.5.7 item's `subagentId`; opaque string). */ + subagentId: string; +} + +/** A subagent's result envelope (tdd SS4.5.7, `SubagentResultEnvelope`). */ +export interface SubagentResult { + /** Artifact references, verbatim. */ + artifactRefs: string[]; + /** Error kind, verbatim durable vocabulary, when the child failed. */ + errorKind?: string; + /** Evidence references, verbatim. */ + evidenceRefs: string[]; + /** Structured result data, verbatim, when present. */ + structuredData?: Record; + /** Bounded result summary (<=512 chars, runtime-enforced). */ + summary: string; + /** Result text (<=32 KiB), when present. */ + text?: string; +} + +/** Params for `subagent/resume`, `subagent/reopen`, and `subagent/readResult` (SS3.16): the bare child target. */ +export interface SubagentTargetParams { + /** Client-minted UUIDv7 command id (SS3.1.1). */ + commandId: string; + /** Target session (UUIDv7 string; SS3 preamble). */ + sessionId: string; + /** Durable child id (the SS4.5.7 item's `subagentId`; opaque string). */ + subagentId: string; +} + +/** The one usage payload shape: the `usage/read` result's `usage` member and the `usage/changed` params (ADR 32563 D2/D3). The numbers are point-in-time — `observedAtMs` is the host's arrival stamp, so a client renders "as of", never implies live data (spec 18742 US-FR-004). */ +export interface SubscriptionUsage { + /** When the host RECEIVED this observation (frame arrival or mint request-send), epoch milliseconds. */ + observedAtMs: number; + /** The provider's subscription tier id, verbatim. */ + tier: string; + /** The rolling weekly block. */ + weekly: SubscriptionUsageWeekly; + /** The current window. */ + window: SubscriptionUsageWindow; +} + +/** The rolling weekly block: same semantics as [`SubscriptionUsageWindow`] without a duration (ADR 32563 D2). */ +export interface SubscriptionUsageWeekly { + /** When the weekly window resets, epoch milliseconds. */ + resetsAtMs: number; + /** Percent of the weekly budget used (integer ≥ 0, may exceed 100). */ + usedPercent: number; +} + +/** The current usage window (the provider's 5-hour-class block): verbatim provider percentages with the reset stamp normalized to epoch milliseconds (ADR 32563 D2). */ +export interface SubscriptionUsageWindow { + /** When the window resets, epoch milliseconds. */ + resetsAtMs: number; + /** Percent of the window's budget used: an integer ≥ 0, verbatim from the provider — over-quota values above 100 are valid. */ + usedPercent: number; + /** The window's length in minutes (> 0). */ + windowDurationMins: number; +} + +/** A success response frame (SS1.2 §2.3). */ +export interface SuccessResponse { + /** Echo of the request id. */ + id: RequestId; + /** The literal `"2.0"`. */ + jsonrpc: JsonRpcVersion; + /** Always a JSON object, possibly `{}`, never a bare scalar — so every result can grow additive-optional members (SS1.2). */ + result: Record; +} + +/** `task/background` params (tdd §3.13): durably send a running foreground tool task to the background. */ +export interface TaskBackgroundParams { + /** The SS3.1.1 idempotency handle (UUIDv7). */ + commandId: string; + /** The target session. */ + sessionId: string; + /** The task to background, exactly as the view names it: the `toolCall` item's `itemId` IS the task id (SS4.5.5). */ + taskId: string; +} + +/** The `task/background` / `task/stop` ack (tdd §3.13/§3.14): admission-only, echoing the targeted task. */ +export interface TaskCommandResult { + /** Echoes the client's id. */ + commandId: string; + /** Admission status. */ + status: CommandStatus; + /** Echoes the targeted task id. */ + taskId: string; +} + +/** `task/stopAll` params (tdd §3.15): stop every stoppable background workload live at admission. */ +export interface TaskStopAllParams { + /** The SS3.1.1 idempotency handle (UUIDv7). */ + commandId: string; + /** The target session. */ + sessionId: string; +} + +/** The `task/stopAll` ack (tdd §3.15): always `accepted`, including over an empty set — a blanket stop over nothing is a satisfied gesture. The ack deliberately carries no stopped-task list; count the kills from the view. */ +export interface TaskStopAllResult { + /** Echoes the client's id. */ + commandId: string; + /** Admission status. */ + status: CommandStatus; +} + +/** `task/stop` params (tdd §3.14): stop one named background task. Required target, never "whichever is loudest" — §3.15 is the blanket verb. */ +export interface TaskStopParams { + /** The SS3.1.1 idempotency handle (UUIDv7). */ + commandId: string; + /** The target session. */ + sessionId: string; + /** The task to stop, the `toolCall` item's `itemId` (SS4.5.5). */ + taskId: string; +} + +/** One todo entry (tdd SS4.6.3). */ +export interface TodoItem { + /** Present-tense active form, when the tool supplied one. */ + activeForm?: string; + /** The todo status (camelCased runtime `TodoStatus`). */ + status: TodoStatus; + /** The todo text. */ + text: string; +} + +/** The latest todo-list fact in the snapshot (tdd SS4.6.3, SS4.9.1). */ +export interface TodoListState { + /** The full todo list. */ + items: TodoItem[]; + /** The snapshot revision. Diagnostics only, never an ordering guard. */ + revision: number; + /** The tool that produced the snapshot, verbatim. */ + sourceTool: string; +} + +/** Todo status (tdd SS4.6.3): closed in the runtime, wire-open. */ +export type TodoStatus = "pending" | "inProgress" | "completed" | "cancelled" | (string & {}); + +/** Raw token counters, verbatim from the durable record (tdd SS4.6.5, the runtime's `Usage`, camelCased). Not directly summable across providers — sum the counted-once derivations instead (#8803). */ +export interface TokenUsage { + /** Cache read tokens, only when the provider distinguishes writes/reads. */ + cacheReadTokens?: number; + /** Cache write tokens, only when the provider distinguishes writes/reads. */ + cacheWriteTokens?: number; + /** Provider-reported cache tokens (inside or beside `inputTokens`, provider-convention-dependent — the reason `promptTokens` exists). */ + cachedTokens: number; + /** Provider-reported input tokens. */ + inputTokens: number; + /** Provider-reported output tokens. */ + outputTokens: number; + /** Output tokens spent on reasoning, when the provider reports it. */ + reasoningTokens: number; +} + +/** Optional W3C trace context, on requests in both directions only — never on responses or notifications (SS1.8). */ +export interface TraceContext { + /** W3C `traceparent` string; receivers that do not trace ignore it. */ + traceparent?: string; + /** W3C `tracestate` string. */ + tracestate?: string; +} + +/** `turn/cancel` params (tdd SS3.5): plain cancellation on the normal command lane. Same shape as `turn/interrupt`, **without** `retract` — pairing a retract with a plain cancel is durably rejected `not_paired_interrupt`. */ +export interface TurnCancelParams { + /** The SS3.1.1 idempotency handle (UUIDv7). */ + commandId: string; + /** The target session. */ + sessionId: string; + /** The exact turn to cancel; omit to target the current foreground turn. Any syntactically valid UUID: a turn from an older session may carry a v4 or v5 id. A malformed id is `invalidParams`. */ + turnId?: string; +} + +/** `turn/cancel` result (tdd SS3.5): identical envelope to `turn/interrupt`. */ +export interface TurnCancelResult { + /** Echoes the client's id. */ + commandId: string; + /** Admission status. */ + status: CommandStatus; + /** The cancelled turn. */ + turnId: string; +} + +/** `turn/completed` params (tdd SS4.5.1): the run's durable terminal record landed. */ +export interface TurnCompletedParams { + /** Turn duration; absent means unmeasured, never fabricated. */ + durationMs?: number; + /** Present iff `terminal` is `"failed"` (tdd SS4.5.1): mid-turn failures reach the client here — never as a JSON-RPC error. */ + error?: TurnError; + /** The runtime's free-text terminal reason, verbatim. Display and diagnostics only; never branch on it. */ + reason?: string; + /** The owning session. */ + sessionId: string; + /** The durable records this event folded from. */ + sourceRange: SourceRange; + /** The turn terminal (open enum). */ + terminal: TurnTerminal; + /** Time to first token, when measured. */ + timeToFirstTokenMs?: number; + /** The terminated turn. */ + turnId: string; + /** The turn's aggregate token usage, summed across the turn's model completions (same shape as `session/tokenUsage.usage`). */ + usage?: TokenUsage; + /** Opaque, strictly monotonic view cursor (tdd SS4.1). */ + viewCursor: string; +} + +/** The settled turn-failure object (tdd SS4.5.1): `{kind, message, retryable}`. */ +export interface TurnError { + /** The failure class (open camelCase enum grounded in the runtime's terminal failure classification). */ + kind: TurnErrorKind; + /** Human-readable failure text. */ + message: string; + /** The server's judgment that resubmitting the same input may succeed. */ + retryable: boolean; +} + +/** Turn failure classes (tdd SS4.5.1, `TerminalErrorKind` plus the model-task failure class). Open. */ +export type TurnErrorKind = + | "stepLimit" + | "configError" + | "projectionError" + | "logError" + | "workflowLaunchError" + | "environmentError" + | "modelError" + | "launchError" + | "authRequired" + | (string & {}); + +/** One ordered content part of a turn submission (tdd SS3.2). File mentions are text, not a part type: write `@relative/path` in a text part. A structured `mention` part is reserved and currently rejected, and an unknown part type is `invalidParams` (tdd SS3.1.2, SS3.2) — which is what closes [`TurnInputPartType`]. Modelled as a discriminated flat object rather than a Rust `enum`, the convention [`crate::view::approval::ApprovalSubject`] already established for a wire union in this crate: the v1 schema model names no object-variant union shape and fails closed on one. The serialized JSON is the tdd shape either way; what a flat object cannot express is "`mediaType` is required exactly when `type` is `image`". **RULED (#22785 E4, owner, 2026-08-26): the precedent is accepted.** A strict union node kind may be funded later as a follow-up; if it is, it must cover [`crate::method::lifecycle::PendingRequestPointer`] too. `height` may only appear together with `width`. `width` may only appear together with `height`. */ +export interface TurnInputPart { + /** Free-text skill arguments, optional on a `skill` part — the wire twin of what the TUI accepts after the shortcut token (tdd SS3.22.3). */ + arguments?: string; + /** Base64 payload, required on an `image` part. Invalid base64 or an empty payload is rejected with invalid params. */ + base64Data?: string; + /** Pixel height; must be provided together with `width` or not at all (tdd SS3.2, #22785 E6c). */ + height?: number; + /** Media type, required on an `image` part. */ + mediaType?: string; + /** The skill to invoke, required on a `skill` part (tdd SS3.2, SS3.22.3; ADR 32471 D4): a bare or plugin-qualified shortcut token from `skill/list`. The HOST resolves and expands; an unknown selector is the typed `-32032 skillNotFound` request error. */ + selector?: string; + /** User prompt text, on a `text` part. Multiple text parts are joined in order into the turn's prompt. */ + text?: string; + /** The part type. */ + type: TurnInputPartType; + /** Pixel width; must be provided together with `height` or not at all (tdd SS3.2). Published as a `dependentRequired` pair under #22785 E6c. */ + width?: number; +} + +/** The `type` discriminator of a turn input part (tdd SS3.2). Closed: an unknown part type is `invalidParams` (tdd SS3.1.2). */ +export type TurnInputPartType = "text" | "image" | "skill"; + +/** `turn/interrupt` params (tdd SS3.4): the "user pressed stop" gesture, on the runtime's priority lane. */ +export interface TurnInterruptParams { + /** The SS3.1.1 idempotency handle (UUIDv7). */ + commandId: string; + /** Pair a retract intent with the interrupt: if the turn is cancelled before any assistant output committed, the submission is durably retracted (view event `turn/retracted`) so the client may restore the prompt text. A rejected retract does not undo the interrupt. Default `false` (tdd SS3.4). */ + retract?: boolean; + /** The target session. */ + sessionId: string; + /** The exact turn to interrupt. Omit to target the session's current foreground turn, resolved at admission; prefer passing the explicit id when you have one (tdd SS3.4). Any syntactically valid UUID: a turn from an older session may carry a v4 or v5 id. A malformed id is `invalidParams`. */ + turnId?: string; +} + +/** `turn/interrupt` result (tdd SS3.4). Acceptance means the interrupt was admitted, **not** that the turn is already stopped: the turn is over when you fold its `turn/completed` with terminal `cancelled`. */ +export interface TurnInterruptResult { + /** Echoes the client's id. */ + commandId: string; + /** Admission status. */ + status: CommandStatus; + /** The interrupted turn. */ + turnId: string; +} + +/** A turn named by the snapshot's active/queued lists (tdd SS4.9.1). */ +export interface TurnRef { + /** The `commandId` of the submit that minted it. */ + commandId: string; + /** The turn's id, pre-minted per tdd SS3.1.4. */ + turnId: string; +} + +/** `turn/retracted` params (tdd SS4.5.1): an interrupt-paired retract was durably accepted. The retracted user-message item is re-emitted via `item/updated` with `retracted: true`. */ +export interface TurnRetractedParams { + /** The retracted submission's command id. */ + commandId: string; + /** The owning session. */ + sessionId: string; + /** The durable records this event folded from. */ + sourceRange: SourceRange; + /** The turn whose submission was retracted. */ + turnId: string; + /** Opaque, strictly monotonic view cursor (tdd SS4.1). */ + viewCursor: string; +} + +/** `turn/retryScheduled` params (tdd SS4.5.1, spec 14412; D-026): a durable turn retry-scheduled fact folded — the failing model attempt's scheduled retry is observable BEFORE the turn terminates, so a streaming client can render "attempt N/M · retrying in Ss · reason" instead of dead air. Non-terminal: it never resolves a turn-wait (tdd SS3.1.4). The delay is the recorded scheduled backoff, never an absolute fire time — clients derive any countdown locally. `sourceRange` cites the session stream's mirror of the task-lifecycle record (tdd SS4.2). */ +export interface TurnRetryScheduledParams { + /** The attempt that failed (producer-normalized, >= 1). */ + attempt: number; + /** The turn's model-retry chain bound. */ + maxAttempts: number; + /** The attempt about to run (producer contract: attempt < nextAttempt <= maxAttempts; not a schema constraint). */ + nextAttempt: number; + /** The recorded stopping reason through the shared task-detail normalization (non-empty; capped at 160 characters). */ + reason: string; + /** The scheduled backoff delay in milliseconds, verbatim from the durable fact. */ + retryDelayMs: number; + /** The owning session. */ + sessionId: string; + /** The durable record this event folded from (the session stream's mirror of the retry-scheduled task status; `first == last`). */ + sourceRange: SourceRange; + /** The running turn whose model attempt is being retried. */ + turnId: string; + /** Opaque, strictly monotonic view cursor (tdd SS4.1). */ + viewCursor: string; +} + +/** What `turn/start` did with the input (tdd SS3.2). Resolves OQ-H in v1: since `queue` is the default `ifBusy`, clients need to distinguish these without folding history. */ +export type TurnStartDisposition = "started" | "queued" | "steered" | (string & {}); + +/** `turn/start.providerRequestOptions` is deliberately absent: **RULED (#22785 E3, owner, 2026-08-26) to stay off the published schema.** The schema is the contract, and a free-form-object node kind is revisited only if the experimental field graduates. `turn/start` params (tdd SS3.2). */ +export interface TurnStartParams { + /** The SS3.1.1 idempotency handle (UUIDv7). The fresh turn's `turnId` derives from it (tdd SS3.1.4). */ + commandId: string; + /** Presentation form of the prompt for transcripts. Durable; carried on the resulting user-message view item. Never model-visible (tdd SS3.2). */ + displayText?: string; + /** Disposition when a turn is already running; default `queue`. */ + ifBusy?: IfBusy; + /** Ordered content parts; required and non-empty. */ + input: TurnInputPart[]; + /** Reasoning tier sampled at submission for this turn. */ + reasoningEffort?: ReasoningEffort; + /** The target session. */ + sessionId: string; + /** A sticky REPLACEMENT of the session's runtime workspace-root set, binding at this turn's launch and persisting for subsequent turns until replaced again — unlike `reasoningEffort`, which is this-turn-only. Same shape and validation as `session/start.workspaceRoots`; invalid params on an explicit `ifBusy: "steer"` submit and so is explicit `null` (the D-049 null-tolerant set is closed); omitted means "unchanged", never "reset" (tdd SS3.2, ADR 37313 D3, #37313). */ + workspaceRoots?: string[]; +} + +/** `turn/start` result (tdd SS3.2). */ +export interface TurnStartResult { + /** Echoes the client's id. */ + commandId: string; + /** What happened to the input. */ + disposition: TurnStartDisposition; + /** `true` iff `disposition` is `started`; retained as the boolean shorthand — `disposition` is authoritative (tdd SS3.2). */ + startedNewTurn: boolean; + /** Admission status. */ + status: CommandStatus; + /** The turn that will carry (or absorbed) this input. Authoritative: always take it from the ack rather than deriving it (tdd SS3.2). */ + turnId: string; +} + +/** `turn/started` params (tdd SS4.5.1): a foreground turn began running — fresh submits immediately, queued submits at their launch boundary, never steered submits. */ +export interface TurnStartedParams { + /** The submitting command; `turnId == commandId` for fresh turns (tdd SS3.1.4). */ + commandId: string; + /** The owning session. */ + sessionId: string; + /** The durable records this event folded from. */ + sourceRange: SourceRange; + /** The started turn. */ + turnId: string; + /** Opaque, strictly monotonic view cursor (tdd SS4.1). */ + viewCursor: string; +} + +/** `turn/steer` params (tdd SS3.3): exact-target steering into the currently running turn. */ +export interface TurnSteerParams { + /** The SS3.1.1 idempotency handle (UUIDv7). */ + commandId: string; + /** The turn you believe is running. Closes the race where the turn completes or is replaced between your read and your steer: input meant for turn A can never leak into turn B (tdd SS3.3). Any syntactically valid UUID: a turn from an older session may carry a v4 or v5 id. A malformed id is `invalidParams`; an id that is not the running turn is refused. */ + expectedTurnId: string; + /** Same content parts as `turn/start`. */ + input: TurnInputPart[]; + /** Reasoning tier for the model calls this steer's input reaches. Applies forward, never backward; absent means unspecified, not "reset" (tdd SS3.3, D-024). */ + reasoningEffort?: ReasoningEffort; + /** The target session. */ + sessionId: string; +} + +/** `turn/steer` result (tdd SS3.3). */ +export interface TurnSteerResult { + /** Echoes the client's id. */ + commandId: string; + /** Admission status. */ + status: CommandStatus; + /** The running turn that absorbed the input. */ + turnId: string; +} + +/** Turn terminal vocabulary (tdd SS4.5.1): exactly the runtime's `RunTerminalKind` — closed in the runtime, wire-open for evolution. */ +export type TurnTerminal = "completed" | "failed" | "cancelled" | (string & {}); + +/** `turn/unqueue` params (tdd SS3.6): reclaim a queued submit before it launches. It is **not** a stop — a reclaim that arrives after its target launched is durably rejected, never silently upgraded into a cancel. */ +export interface TurnUnqueueParams { + /** The SS3.1.1 idempotency handle (UUIDv7). */ + commandId: string; + /** The target session. */ + sessionId: string; + /** The queued turn to reclaim, exactly as the queueing `turn/start`'s ack minted it and as snapshot `queuedTurns` lists it. Required, and never "whichever is newest" (tdd SS3.6). Any syntactically valid UUID: a turn from an older session may carry a v4 or v5 id. A malformed id is `invalidParams`. */ + turnId: string; +} + +/** `turn/unqueue` result (tdd SS3.6). Admission only — but for this command admission *is* the race: an admitted reclaim implies the turn will not launch. The authoritative removal is the `turn/unqueued` view event. */ +export interface TurnUnqueueResult { + /** Echoes the client's id. */ + commandId: string; + /** Admission status. */ + status: CommandStatus; + /** The reclaimed turn. */ + turnId: string; +} + +/** `turn/unqueued` params (tdd SS3.6, SS4.5.1): a queued submit's reclaim durably won; its pre-minted turn never runs. Carries the reclaimed `turnId` and the `commandId` of the `turn/start` that queued it, so a client can restore the submission's text exactly as for `turn/retracted`. No `turn/started`/`turn/completed` is ever emitted for this `turnId` (SDK turn-waits settle on this event too, tdd SS3.1.4). `sourceRange` cites the SESSION stream's reclaim record — a reclaimed turn never had a run stream. */ +export interface TurnUnqueuedParams { + /** The `commandId` of the `turn/start` that queued the reclaimed turn. */ + commandId: string; + /** The owning session. */ + sessionId: string; + /** The durable records this event folded from (the session stream's reclaim record). */ + sourceRange: SourceRange; + /** The reclaimed turn: pre-minted at admission, never launched. */ + turnId: string; + /** Opaque, strictly monotonic view cursor (tdd SS4.1). */ + viewCursor: string; +} + +/** One element of a `view/page` result's `events` array: an **unframed view notification** exactly as tdd SS4.2.1 defines it — the nested pair `{"method", "params"}`, which is the live notification minus `jsonrpc` and `emittedAtMs`, with nothing lifted out of `params` and nothing spliced into it. SS4.2.1 is the single normative definition; SS4.7.3 and SS7.4 both cite it, so the two surfaces cannot drift. */ +export interface UnframedViewNotification { + /** The event type — the live notification's method name, retained because the type lives only in the notification name and would otherwise be lost (tdd SS4.2.1). */ + method: string; + /** The notification's params object, verbatim. */ + params: UnframedViewNotificationParams; +} + +/** The `params` of an unframed view notification (tdd SS4.2.1, SS4.2). Declares the SS4.2 base members every view notification carries, and stays **open** for the event-specific fields SS4.5/SS4.6/SS5 add per event type. The v1 schema model names no method-correlated union (the SS4.7.3 `events` element's `params` arm is selected by its sibling `method`), so the per-event arms cannot be expressed here without one; enumerating them as an uncorrelated union would additionally exclude `turn/unqueued`, whose notification row is another lane's ratified deferral (#207/#14407) — a published schema that rejects a real page element is the exact #19923 defect this PR exists to close. Recorded as a Design delta on #22785. `sourceRange` is required here and not optional: `view/page` serves durable-sourced events only (tdd SS4.7.3), and durable-sourced is *defined* as carrying a `sourceRange` — the SS4.2 ephemeral-sourced exemption applies to `item/delta`, which `view/page` never replays. */ +export interface UnframedViewNotificationParams { + /** The owning session (tdd SS4.2). */ + sessionId: string; + /** The durable records this event folded from (tdd SS4.2, SS6.8). */ + sourceRange: SourceRange; + /** The event's opaque, strictly monotonic view cursor. It stays **inside** `params`, where every live notification already carries it (tdd SS4.2.1) — nothing is spliced beside `method`. */ + viewCursor: string; +} + +/** The only scalar alternative to a known effort array. Kept as a closed singleton so arbitrary strings and uppercase spellings fail decoding. */ +export type UnknownModelReasoningEffortVariants = "unknown"; + +/** `usage/read` result: `{usage?}` — omitted, never `null`, when the host has observed nothing (ADR 32563 D2: truthful absence, no "nothing observed" error). */ +export interface UsageReadResult { + /** The last-observed subscription usage window, when one exists. */ + usage?: SubscriptionUsage; +} + +export interface UserInputAnswer { + freeText?: string; + note?: string; + questionId: string; + selectedLabel?: string; + selectedLabels?: string[]; +} + +/** `userInput/answer` params (tdd SS5.10.2): answer every question. Each answer carries `questionId` then exactly one of `selectedLabel` (single mode), `selectedLabels` (multiple mode, within min/max bounds), or `freeText` (<=500 chars); plus an optional `note` (<=500). Answers that do not match the prompt fail -32057 `userInputAnswerInvalid`. Image `attachments` are a **reserved** field in v1, rejected if sent, because the blob-upload path they reference is reserved in tdd SS3.2. */ +export interface UserInputAnswerParams { + /** One entry per question. */ + answers: UserInputAnswer[]; + /** The SS3.1.1 idempotency handle (UUIDv7). */ + commandId: string; + /** The target session. */ + sessionId: string; + /** The prompt being answered. */ + userInputId: string; +} + +/** `userInput/answer` result (tdd SS5.10.2). */ +export interface UserInputAnswerResult { + /** Echoes the client's id. */ + commandId: string; + /** Admission status. */ + status: CommandStatus; + /** The prompt this answer settled. */ + userInputId: string; +} + +/** `userInput/cancel` params (tdd SS5.10.2): decline to answer; the tool call resolves with a cancelled result the model sees. */ +export interface UserInputCancelParams { + /** The SS3.1.1 idempotency handle (UUIDv7). */ + commandId: string; + /** Why the prompt was declined. */ + reason?: string; + /** The target session. */ + sessionId: string; + /** The prompt being declined. */ + userInputId: string; +} + +/** `userInput/cancel` result (tdd SS5.10.2). */ +export interface UserInputCancelResult { + /** Echoes the client's id. */ + commandId: string; + /** Admission status. */ + status: CommandStatus; + /** The prompt this cancellation settled. */ + userInputId: string; +} + +export interface UserInputClarification { + content: string; + format: string; +} + +/** `userInput/clarify` params (tdd SS5.10.2): answer with a free-form clarification instead of the structured options — the "let me explain" path; the model receives the clarification text and re-decides. */ +export interface UserInputClarifyParams { + /** `format` is `"text"` in v1 (open); `content` is <=500 chars. */ + clarification: UserInputClarification; + /** The SS3.1.1 idempotency handle (UUIDv7). */ + commandId: string; + /** The target session. */ + sessionId: string; + /** The prompt being clarified. */ + userInputId: string; +} + +/** `userInput/clarify` result (tdd SS5.10.2). */ +export interface UserInputClarifyResult { + /** Echoes the client's id. */ + commandId: string; + /** Admission status. */ + status: CommandStatus; + /** The prompt this clarification settled. */ + userInputId: string; +} + +export interface UserInputOption { + description?: string; + label: string; + preview?: UserInputOptionPreview; +} + +export interface UserInputOptionPreview { + content: string; + format: string; +} + +export type UserInputOutcome = + | "answered" + | "cancelled" + | "interrupted" + | "clarified" + | "timedOut" + | "aborted" + | (string & {}); + +export interface UserInputQuestion { + header: string; + id: string; + options: UserInputOption[]; + question: string; + selection: UserInputSelection; +} + +/** Full params shared by `userInput/request` and `userInput/requested`. */ +export interface UserInputRequestParams { + autoResolutionMs?: number; + itemId: string; + questions: UserInputQuestion[]; + sessionId: string; + sourceRange?: SourceRange; + toolCallId: string; + toolName: string; + turnId: string; + userInputId: string; + viewCursor: string; +} + +export interface UserInputSelection { + maxSelections?: number; + minSelections?: number; + mode: UserInputSelectionMode; +} + +export type UserInputSelectionMode = "single" | "multiple"; + +/** `userInput/settled` params: the first durable prompt settlement. */ +export interface UserInputSettledParams { + answers: UserInputAnswer[]; + clarification: UserInputClarification | null; + decidedByCommandId: string | null; + outcome: UserInputOutcome; + reason: string | null; + sessionId: string; + sourceRange: SourceRange; + userInputId: string; + viewCursor: string; +} + +/** Winning terminal returned to a late user-input command. */ +export interface UserInputSettlementSummary { + outcome: string; + viewCursor: string; +} + +/** Version-control system of a branch observation (tdd SS4.6.4). Open. */ +export type Vcs = "git" | "sapling" | (string & {}); + +/** `view/gap` params (spec 208 SS4.8, FM-001): push delivery dropped events, and this names the hole. The bracket is deferred (D-16487-2): it flushes at the subscription's next ACCEPTED delivery, never at enqueue time, so `next` always names an event whose own delivery is proven. A run of consecutive undelivered events coalesces into one bracket keeping the FIRST range's `after` (D-16487-1). The gap rides the protected reserved queue, so no wire-interleaving order against neighbouring deliveries is promised — the cursors carry the semantics (INV-002, D-16487-3). On receipt a client may take either sanctioned recovery (D-030, FR-013): splice-fill — buffer live events at cursors >= `next`, page `(after, next)` forward, discard the overlap, splice — or re-anchor through the anchored read surface. The server requires neither, cannot distinguish clients by their choice, and holds no partial-fill state. */ +export interface ViewGapParams { + /** The last cursor delivered before the hole — the exclusive lower bound of the undelivered range. An **opaque** view cursor: clients relay it, never parse it (tdd SS4.1). */ + after: string; + /** The first cursor delivered after the hole — the exclusive upper bound of the undelivered range, and the position delivery continues from. An **opaque** view cursor: clients relay it, never parse it (tdd SS4.1). */ + next: string; + /** The session whose subscription dropped events. */ + sessionId: string; +} + +/** The `view/page` request-mode anchor (tdd SS4.7.3). Declared **open**: the tdd spells it "open string enum, v1 value `latestCompaction`". */ +export type ViewPageAnchor = "latestCompaction" | (string & {}); + +/** The `view/page` paging direction (tdd SS4.7.3). Closed: the tdd spells exactly two values and a third would change the paging contract. */ +export type ViewPageDirection = "forward" | "backward"; + +/** `view/page` params (tdd SS4.7.3): cursor-paged reads of the session view. */ +export interface ViewPageParams { + /** The cold-client request mode (D-029; spec 208 FR-008), resolved at call time to the latest installed compaction boundary. The one named exception to the SS4.1 observed-only rule. */ + anchor?: ViewPageAnchor; + /** The exclusive anchor. Forward: the page starts strictly after it; omitted means from the beginning of the view. Backward: the page ends strictly before it; omitted means from the head. Mutually exclusive with `anchor` (tdd SS4.7.3; spec 208 FR-008a). */ + cursor?: string; + /** Paging direction; `forward` when omitted. */ + direction?: ViewPageDirection; + /** Maximum events to return, 1–1000. The server MAY return fewer (tdd SS4.7.3's short-serve rule). The 1–1000 range is ratified and now **published**: #22785 E6c is owner-ruled, so the model carries the bound and the bundle renders `minimum`/`maximum` instead of a bare `integer`. Range 1–1000 inclusive. */ + limit: number; + /** The session to page. */ + sessionId: string; +} + +/** The `resolvedAnchor` echo (tdd SS4.7.3): subsequent pages pass ordinary cursors, so the anchor never needs re-resolving. */ +export interface ViewPageResolvedAnchor { + /** The view cursor of the resolved boundary's `compaction` event. */ + boundaryCursor: string; +} + +/** `view/page` result (tdd SS4.7.3). */ +export interface ViewPageResult { + /** The page's events, always ascending by `viewCursor` in both directions. Durable-sourced only; pages are contiguous and can never skip a cursor. */ + events: UnframedViewNotification[]; + /** The anchor for the next page in the same direction: forward, the last event's cursor; backward, the first's. `null` at the end of the view in that direction — never omitted. */ + nextCursor: string | null; + /** The resolution echo, present exactly when the request used `anchor` (tdd SS4.7.3; spec 208 FR-008). */ + resolvedAnchor?: ViewPageResolvedAnchor; +} + +/** The `snapshot` object returned by `session/resume` / `session/read` (tdd SS4.9). */ +export interface ViewSnapshot { + /** Present exactly when `history.mode` is `anchoredSnapshot` (tdd SS2.5.2). */ + anchor?: SnapshotAnchor; + /** Snapshot schema version. A client whose generated types are older than this must fall back to inline/paged history (tdd SS4.9.2, SS2.5.2). */ + schemaVersion: number; + /** The complete folded view at `viewCursor` (tdd SS4.9.1). */ + state: SnapshotState; + /** The cursor the state is folded at; every event after it streams as the suffix (tdd SS4.9). */ + viewCursor: string; +} + +/** `view/subscribe` params (tdd SS4.7.1): attach this connection's live view subscription for a session at an explicit cursor — the fine-grained counterpart of the SS2 auto-subscribe, and the gap-free re-attachment path after `view/unsubscribe`. It never loads a session, takes no lease, writes nothing. */ +export interface ViewSubscribeParams { + /** Resume cursor. When present, the server replays `(after, head]` as ordinary view notifications before any live event — one gapless cursor sequence. Omitted means "from now": no historical replay (tdd SS4.7.1). */ + after?: string; + /** The session to follow. */ + sessionId: string; +} + +/** `view/subscribe` result (tdd SS4.7.1). */ +export interface ViewSubscribeResult { + /** The view head at subscribe time; the client knows it is live once cursors pass it. */ + viewCursor: string; +} + +/** `view/unsubscribe` params (tdd SS4.7.2): remove this connection from the session's view subscription set — the protocol's single unsubscribe operation, at the altitude that owns subscriptions (tdd SS2.5.6 points here). It does not unload the session; unloading is the idle policy (tdd SS2.8). */ +export interface ViewUnsubscribeParams { + /** The session to stop following. */ + sessionId: string; +} + +/** `view/unsubscribe` result (tdd SS4.7.2): the empty object. Idempotent — unsubscribing while not subscribed returns `{}`. */ +export interface ViewUnsubscribeResult {} + +/** `workflow/cancel` params (tdd SS3.19): cancel a live workflow run. */ +export interface WorkflowCancelParams { + /** The SS3.1.1 idempotency handle (UUIDv7). */ + commandId: string; + /** The target session. */ + sessionId: string; + /** The run to cancel, exactly as the `workflow` item names it (SS4.5.8); required, non-empty. */ + workflowRunId: string; +} + +/** One workflow child's folded state (tdd SS4.5.8, `WorkflowChildLifecycleFact`), keyed by `(childId, attempt)`. */ +export interface WorkflowChild { + /** The attempt number. */ + attempt: number; + /** The child's id within the run. */ + childId: string; + /** The child's observed duration, when recorded. */ + durationMs?: number; + /** The child's display label, when recorded. */ + label?: string; + /** The child's phase, when recorded. */ + phase?: string; + /** The child's recorded result reference, verbatim, when present — an opaque string, matching the durable owner. */ + resultRef?: string; + /** camelCased `WorkflowChildLifecycleStatus`, verbatim (durable runtime vocabulary). */ + status: string; + /** The child's terminal, turn vocabulary (tdd SS4.5.8). */ + terminal?: TurnTerminal; + /** The child's observed usage, when recorded. */ + usage?: TokenUsage; +} + +/** Which control to apply to the child's current attempt (tdd SS3.20). Closed on the wire: an unknown value is `-32602 invalidParams`. */ +export type WorkflowChildAction = "skip" | "retry"; + +/** `workflow/childControl` params (tdd SS3.20): skip or retry one workflow child, keyed by the `(childId, attempt)` pair the `workflow` item's `children[]` carries. */ +export interface WorkflowChildControlParams { + /** The control to apply. */ + action: WorkflowChildAction; + /** The child's CURRENT attempt (integer >= 1). A stale value rejects `"stale_attempt"`; re-read the item and re-key — never guess. Minimum 1. */ + attempt: number; + /** The child, from the item's `children[]`; required, non-empty. */ + childId: string; + /** The SS3.1.1 idempotency handle (UUIDv7). */ + commandId: string; + /** The target session. */ + sessionId: string; + /** The run owning the child (SS4.5.8); required, non-empty. */ + workflowRunId: string; +} + +/** The shared SS3.19/SS3.20 admission-only ack: deliberately bare `{commandId, status}` — settlement arrives as the workflow item's view events, never through the ack. */ +export interface WorkflowControlResult { + /** Echoes the client's id. */ + commandId: string; + /** Admission status. */ + status: CommandStatus; +} + +/** Every wire method in this schema (SS1.9 index). */ +export type MspMethod = + | "initialize" + | "subagent/sendMessage" + | "subagent/followupTask" + | "subagent/interrupt" + | "subagent/stop" + | "subagent/resume" + | "subagent/reopen" + | "subagent/close" + | "subagent/readResult" + | "session/start" + | "session/resume" + | "session/fork" + | "session/list" + | "session/read" + | "turn/start" + | "turn/steer" + | "turn/interrupt" + | "turn/cancel" + | "turn/unqueue" + | "session/compact" + | "session/delete" + | "session/setModel" + | "session/rename" + | "session/setReasoningEffort" + | "session/userShell" + | "model/list" + | "skill/list" + | "task/background" + | "task/stop" + | "task/stopAll" + | "goal/set" + | "goal/edit" + | "goal/clear" + | "goal/pause" + | "goal/resume" + | "workflow/cancel" + | "workflow/childControl" + | "view/subscribe" + | "view/unsubscribe" + | "view/page" + | "item/readOutput" + | "approval/decide" + | "approval/listPending" + | "session/setApprovalMode" + | "userInput/answer" + | "userInput/cancel" + | "userInput/clarify" + | "usage/read"; +/** Every wire notification in this schema (SS1.9 index). */ +export type MspNotification = + | "initialized" + | "session/started" + | "session/closed" + | "session/deleteCompleted" + | "skill/changed" + | "turn/started" + | "turn/completed" + | "turn/retracted" + | "turn/retryScheduled" + | "turn/unqueued" + | "item/started" + | "item/updated" + | "item/delta" + | "item/completed" + | "view/gap" + | "approval/requested" + | "approval/updated" + | "approval/resolved" + | "userInput/requested" + | "userInput/settled" + | "session/modelChanged" + | "session/reasoningEffortChanged" + | "session/statusChanged" + | "session/goalChanged" + | "session/todoListChanged" + | "session/branchChanged" + | "session/tokenUsage" + | "session/contextUsage" + | "session/approvalModeChanged" + | "session/modelRouteUnserved" + | "session/nameChanged" + | "session/viewHealthChanged" + | "session/listChanged" + | "usage/changed"; +/** Every server-initiated wire request in this schema (SS5.3/SS5.10.1 index). */ +export type MspServerRequest = "approval/request" | "userInput/request"; +/** Every error `data.kind` in this schema's error table (SS1.6) — each code's primary kind plus its override kinds. */ +export type MspErrorDataKind = + | "parseError" + | "invalidRequest" + | "notInitialized" + | "alreadyInitialized" + | "methodNotFound" + | "experimentalRequired" + | "invalidParams" + | "internal" + | "pageEventTooLarge" + | "outputResultTooLarge" + | "overloaded" + | "inputTooLarge" + | "capabilityRequired" + | "notFound" + | "interrupted" + | "cancelled" + | "sessionNotFound" + | "sessionInUse" + | "sessionAmbiguous" + | "forkBoundaryInvalid" + | "sessionNotLoaded" + | "sessionStreamMismatch" + | "commandRejected" + | "backpressured" + | "skillNotFound" + | "viewTruncated" + | "outputUnavailable" + | "boundaryPruned" + | "boundaryUnusable" + | "noBoundary" + | "approvalNotFound" + | "approvalAlreadyResolved" + | "approvalChoiceInvalid" + | "approvalRequirementStale" + | "approvalReviewerUnavailable" + | "userInputNotFound" + | "userInputAlreadySettled" + | "userInputAnswerInvalid"; diff --git a/plugins/muse-provider/server/provider.ts b/plugins/muse-provider/server/provider.ts new file mode 100644 index 0000000000..10e000b0c1 --- /dev/null +++ b/plugins/muse-provider/server/provider.ts @@ -0,0 +1,223 @@ +import { + negotiateProviderCapabilities, + requireProviderCapabilities, + type ProviderConnection, + type ProviderEvent, + type ProviderInput, + type ProviderLaunch, + type ProviderRegistration, + type ProviderStatus, +} from "@getpaseo/plugin/server/provider"; +import type { PluginSettings } from "@getpaseo/plugin/server"; +import { serveArgs } from "./settings.js"; +import { settingsSchema } from "../shared/settings.js"; +import { Usage } from "./usage.js"; +import { execFile } from "node:child_process"; +import { Catalog, launchKey } from "./catalog.js"; +import { MspConnection } from "./connection.js"; +import { MuseError, actionableError } from "./errors.js"; +import { Sessions } from "./sessions.js"; +import { Session } from "./session.js"; +import { accountSchema } from "./wire.js"; + +const capabilities = [ + "prompt.message", + "prompt.command", + "prompt.image", + "prompt.steer", + "session.configure", + "session.persistence", + "permission", + "session.list", +] as const; + +export function createMuseProvider( + settings: PluginSettings, + usage: Usage, +): ProviderRegistration { + return { + id: "muse", + label: "Muse Code", + icon: "icon.svg", + command: ["muse"], + async getCatalogCacheKey(options) { + return launchKey(requireLaunch(options.launch)); + }, + async status({ launch }) { + if (!launch) + return { available: false, diagnostic: "Install Muse Code and ensure `muse` is on PATH." }; + usage.remember(launch); + return status(launch); + }, + async connect(request) { + if (!request.versions.includes(1)) + throw new MuseError("protocol", "Provider protocol version 1 is required"); + usage.remember(requireLaunch(request.launch)); + return connect( + requireLaunch(request.launch), + negotiateProviderCapabilities(request.capabilities, capabilities), + settings, + usage, + ); + }, + }; +} +function requireLaunch(launch: ProviderLaunch | undefined): ProviderLaunch { + if (!launch) throw new MuseError("missingLaunch", "Muse requires a daemon-resolved executable"); + return launch; +} +async function status(launch: ProviderLaunch): Promise { + let host: MspConnection | undefined; + try { + const versionText = await new Promise((resolve, reject) => { + execFile( + launch.command, + [...launch.args, "--version"], + { env: launch.env, timeout: 3000, maxBuffer: 8192 }, + (error, stdout) => { + if (error) reject(new MuseError("version", error.message)); + else resolve(stdout); + }, + ); + }); + const match = /\b(\d+)\.(\d+)\.(\d+)\b/.exec(versionText); + if (!match) return { available: false, diagnostic: "Muse returned an unrecognized version." }; + const major = Number(match[1]); + const minor = Number(match[2]); + if (major < 1 || (major === 1 && minor < 3)) + return { available: false, diagnostic: `Update Muse Code: found ${match[0]}, need ≥1.3.0` }; + host = new MspConnection({ launch, timeoutMs: 3000 }); + await host.initialize(); + const account = await host.request("account/read", {}, accountSchema); + if (account.state === "loggedOut") + return { available: false, diagnostic: "Run `muse login` or set META_API_KEY" }; + return { available: true }; + } catch (error) { + return { available: false, diagnostic: actionableError(error, launch).message }; + } finally { + await host?.close(); + } +} +function connect( + launch: ProviderLaunch, + negotiated: readonly string[], + settings: PluginSettings, + usage: Usage, +): ProviderConnection { + const listeners = new Set<(event: ProviderEvent) => void>(); + const sessions = new Map(); + const catalog = new Catalog(); + const imports = new Sessions(); + let closed = false; + function emit(event: ProviderEvent): void { + if (!closed) for (const listener of listeners) listener(event); + } + async function dispatch(input: ProviderInput): Promise { + if (input.type === "sessions") { + emit({ + type: "sessions", + requestId: input.requestId, + sessions: await imports.list(launch, input), + }); + return; + } + if (input.type === "catalog") { + emit({ type: "catalog", requestId: input.requestId, catalog: await catalog.read(launch) }); + return; + } + if (input.type === "session.open") { + if (sessions.has(input.sessionId)) + throw new MuseError("duplicateSession", "Muse session is already open"); + const session = new Session({ + id: input.sessionId, + config: input.config, + launch, + emit, + capabilities: negotiated, + serveArgs: await serveArgs(settings), + }); + sessions.set(input.sessionId, session); + try { + await session.open(input); + usage.attach( + input.sessionId, + { ...launch, env: { ...launch.env, ...input.config.env } }, + () => session.readUsage(), + ); + } catch (error) { + sessions.delete(input.sessionId); + await session.close(); + throw error; + } + return; + } + if (!("sessionId" in input)) + throw new MuseError("unsupported", `Muse does not support ${input.type}`); + const session = sessions.get(input.sessionId); + if (!session) throw new MuseError("unknownSession", "Muse session is not open"); + switch (input.type) { + case "session.prompt": + await session.prompt(input.prompt); + return; + case "session.configure": + await session.configure(input.changes); + break; + case "session.permission": + await session.answer(input.permissionId, input.response); + return; + case "session.interrupt": + await session.interrupt(); + break; + case "session.close": + await session.close(); + sessions.delete(input.sessionId); + usage.detach(input.sessionId); + emit({ type: "session.closed", sessionId: input.sessionId }); + break; + default: + throw new MuseError("unsupported", `Muse does not support ${input.type}`); + } + if ("requestId" in input) emit({ type: "request.completed", requestId: input.requestId }); + } + function failed(input: ProviderInput, failure: unknown): void { + const error = actionableError(failure, launch); + if (input.type === "session.prompt") + emit({ + type: "session.prompt_result", + sessionId: input.sessionId, + clientMessageId: input.prompt.clientMessageId, + result: { type: "failed", error }, + }); + else if ("requestId" in input) + emit({ type: "request.failed", requestId: input.requestId, error }); + else if ("sessionId" in input) + emit({ type: "session.runtime_failed", sessionId: input.sessionId, error }); + } + return { + version: 1, + capabilities: negotiated, + async send(input) { + if (closed) throw new MuseError("closed", "Muse connection is closed"); + requireProviderCapabilities(negotiated, input); + queueMicrotask(() => { + if (!closed) void dispatch(input).catch((error: unknown) => failed(input, error)); + }); + }, + onEvent(listener) { + listeners.add(listener); + return () => listeners.delete(listener); + }, + async close() { + if (closed) return; + closed = true; + await Promise.all([ + catalog.close(), + imports.close(), + ...[...sessions.values()].map((session) => session.close()), + ]); + for (const id of sessions.keys()) usage.detach(id); + sessions.clear(); + listeners.clear(); + }, + }; +} diff --git a/plugins/muse-provider/server/questions.ts b/plugins/muse-provider/server/questions.ts new file mode 100644 index 0000000000..cf05f358b9 --- /dev/null +++ b/plugins/muse-provider/server/questions.ts @@ -0,0 +1,86 @@ +import type { ProviderEvent, ProviderPermissionResponse } from "@getpaseo/plugin/server/provider"; +import type { z } from "zod"; +import { MspConnection } from "./connection.js"; +import { MuseError } from "./errors.js"; +import { ackSchema, questionSchema, questionAnswersSchema, settledQuestionSchema } from "./wire.js"; + +export class Questions { + private readonly pending = new Map>(); + private readonly settled = new Set(); + constructor( + private readonly host: MspConnection, + private readonly sessionId: string, + private readonly id: string, + private readonly emit: (event: ProviderEvent) => void, + ) {} + + requested(input: unknown): void { + const request = questionSchema.parse(input); + if (this.settled.has(request.userInputId)) return; + this.pending.set(request.userInputId, request); + this.emit({ + type: "session.permission", + sessionId: this.id, + request: { + id: request.userInputId, + name: request.toolName, + kind: "question", + input: { + questions: request.questions.map((question) => ({ + id: question.id, + header: question.header, + question: question.question, + options: question.options, + multiSelect: question.selection.mode === "multiple", + allowOther: true, + allowEmpty: question.selection.minSelections === 0, + })), + }, + }, + }); + } + resolved(input: unknown): void { + const { userInputId } = settledQuestionSchema.parse(input); + this.settled.add(userInputId); + this.pending.delete(userInputId); + this.emit({ + type: "session.permission_resolved", + sessionId: this.id, + permissionId: userInputId, + }); + } + async answer(id: string, response: ProviderPermissionResponse): Promise { + const request = this.pending.get(id); + if (!request) return false; + if (response.behavior === "deny") { + await this.host.command( + "userInput/cancel", + { sessionId: this.sessionId, userInputId: id, reason: response.message }, + ackSchema, + ); + return true; + } + const supplied = questionAnswersSchema.parse(response.updatedInput); + const answers = request.questions.map((question) => { + const text = supplied.answers[question.header]; + if (text === undefined) throw new MuseError("missingAnswer", `Answer ${question.header}`); + if (question.selection.mode === "single") + return question.options.some((option) => option.label === text) + ? { questionId: question.id, selectedLabel: text } + : { questionId: question.id, freeText: text }; + const labels = text + .split(",") + .map((label) => label.trim()) + .filter(Boolean); + if (labels.every((label) => question.options.some((option) => option.label === label))) + return { questionId: question.id, selectedLabels: labels }; + return { questionId: question.id, freeText: text }; + }); + await this.host.command( + "userInput/answer", + { sessionId: this.sessionId, userInputId: id, answers }, + ackSchema, + ); + return true; + } +} diff --git a/plugins/muse-provider/server/recovery.ts b/plugins/muse-provider/server/recovery.ts new file mode 100644 index 0000000000..0218ef47c4 --- /dev/null +++ b/plugins/muse-provider/server/recovery.ts @@ -0,0 +1,45 @@ +import type { z } from "zod"; +import { MspConnection, type Notification } from "./connection.js"; +import { pageSchema } from "./wire.js"; + +// Two minutes accommodates slow model/tool work; polling repairs a lost live view without cancelling it. +const SILENCE_MS = 2 * 60 * 1000; +export class Recovery { + private timer: ReturnType | undefined; + private active = false; + private closed = false; + private cursor: string; + constructor( + private readonly host: MspConnection, + private readonly sessionId: string, + cursor: string, + private readonly apply: (event: Notification) => Promise, + private readonly onSilent: () => void, + ) { + this.cursor = cursor; + } + + activity(cursor: string | undefined, active: boolean): void { + if (cursor !== undefined) this.cursor = cursor; + this.active = active; + clearTimeout(this.timer); + if (active && !this.closed) this.timer = setTimeout(this.onSilent, SILENCE_MS); + } + async backfill(after = this.cursor): Promise { + let cursor: string | null = after; + do { + const page: z.infer = await this.host.request( + "view/page", + { sessionId: this.sessionId, cursor, direction: "forward", limit: 1000 }, + pageSchema, + ); + for (const event of page.events) await this.apply(event); + cursor = page.nextCursor; + } while (cursor !== null && !this.closed); + this.activity(undefined, this.active); + } + close(): void { + this.closed = true; + clearTimeout(this.timer); + } +} diff --git a/plugins/muse-provider/server/session.ts b/plugins/muse-provider/server/session.ts new file mode 100644 index 0000000000..b152361060 --- /dev/null +++ b/plugins/muse-provider/server/session.ts @@ -0,0 +1,635 @@ +import type { + ProviderEvent, + ProviderInput, + ProviderLaunch, + ProviderPersistence, + ProviderSessionConfig, + ProviderConfigChanges, + ProviderPrompt, + ProviderPermissionResponse, + ProviderMcpServerConfig, + ProviderCatalog, + ProviderToolCallDetail, +} from "@getpaseo/plugin/server/provider"; +import type { z } from "zod"; +import { MspConnection, commandId, type Notification } from "./connection.js"; +import { MuseError, actionableError } from "./errors.js"; +import { Timeline, todoItem } from "./timeline.js"; +import { modes, efforts, presentCatalog } from "./catalog.js"; +import { + pendingSchema, + itemNotificationSchema, + approvalResolvedSchema, + usageSchema, + ackSchema, + gapSchema, + catalogSchema, + approvalModeSchema, + approvalSchema, + deltaSchema, + effortSchema, + notificationSchema, + persistenceSchema, + promptResultSchema, + sessionSchema, + turnSchema, + tokenUsageSchema, + contextUsageSchema, + type WireItem, + type WireApproval, +} from "./wire.js"; +import type { SessionMcpServerConfig, TurnStartParams } from "./msp.js"; + +import { Children } from "./children.js"; +import { Recovery } from "./recovery.js"; +import { Questions } from "./questions.js"; +import { Commands } from "./commands.js"; + +interface SessionOptions { + serveArgs: string[]; + id: string; + config: ProviderSessionConfig; + launch: ProviderLaunch; + emit(event: ProviderEvent): void; + capabilities: readonly string[]; +} + +interface Live { + nativeId: string; + timeline: Timeline; + children: Children; + questions: Questions; + commands: Commands; + recovery: Recovery; +} + +export class Session { + private readonly host: MspConnection; + private liveState: Live | undefined; + private providerId = "meta"; + private cursor: string | undefined; + private config: ProviderSessionConfig; + private catalog: ProviderCatalog = { models: [], modes }; + private readonly approvals = new Map(); + private readonly messages = new Map(); + private readonly turns = new Map(); + private notifications: Promise = Promise.resolve(); + private readonly buffered: Notification[] = []; + private opening = true; + private closing = false; + private firstTurn = true; + private interruptCompletion: { + turnId: string; + resolve(): void; + reject(error: Error): void; + } | null = null; + + constructor(private readonly options: SessionOptions) { + this.config = options.config; + this.host = new MspConnection({ + launch: { ...options.launch, env: { ...options.launch.env, ...options.config.env } }, + cwd: options.config.cwd, + serveArgs: options.serveArgs, + }); + this.host.onNotification((notification) => { + if (this.opening) this.buffered.push(notification); + else this.enqueue(notification); + }); + this.host.onExit((error) => { + if (!this.opening && !this.closing) this.runtimeFailed(error); + }); + } + + async open(input: Extract): Promise { + await this.host.initialize(); + const mcpServers = Object.fromEntries( + Object.entries(this.config.mcpServers).map(([id, server]) => [id, mcpServer(server)]), + ); + let response; + if (input.persistence) { + if (input.persistence.version !== 1) + throw new MuseError("invalidPersistence", "Unsupported Muse persistence version"); + const saved = persistenceSchema.parse(input.persistence.data); + response = await this.host.command( + "session/resume", + { sessionId: saved.sessionId, cursor: saved.cursor, config: { mcpServers } }, + sessionSchema, + ); + this.config = { + ...this.config, + model: this.config.model ?? saved.model, + thinkingOption: this.config.thinkingOption ?? saved.thinkingOption, + }; + this.firstTurn = false; + } else { + response = await this.host.command( + "session/start", + { + workspaceRoot: this.config.cwd, + modelId: this.config.model, + approvalMode: approvalModeSchema.parse(this.config.mode ?? "onRequest"), + config: { mcpServers }, + }, + sessionSchema, + ); + } + if (!response.viewCursor) + throw new MuseError( + "emptyViewCursor", + "Muse returned an empty resume viewCursor; this session cannot be watched.", + ); + const nativeId = response.session.sessionId; + this.liveState = { + nativeId, + children: new Children(this.host, this.options.id, this.config.cwd, this.options.emit), + timeline: new Timeline( + this.host, + nativeId, + this.options.id, + this.options.emit, + (messageCommandId) => this.messages.get(messageCommandId), + ), + questions: new Questions(this.host, nativeId, this.options.id, this.options.emit), + commands: new Commands(this.host, nativeId, this.options.id, this.options.emit), + recovery: new Recovery( + this.host, + nativeId, + response.viewCursor, + (event) => this.notify(event), + () => { + this.notifications = this.notifications + .then(async () => { + const live = this.live(); + await live.recovery.backfill(); + return live.children.refresh(); + }) + .catch((error) => this.runtimeFailed(error)); + }, + ), + }; + this.providerId = response.session.providerId; + this.cursor = response.viewCursor; + this.config = { + ...this.config, + model: response.session.modelId, + mode: response.session.approvalMode?.mode ?? this.config.mode ?? "onRequest", + }; + this.options.emit({ + type: "session.opened", + requestId: input.requestId, + sessionId: this.options.id, + cwd: this.config.cwd, + restoration: "core", + capabilities: this.options.capabilities, + persistence: this.persistence(), + }); + if (input.history === "replay" && response.history?.mode === "inline") { + if (!response.history.items) + throw new MuseError("invalidHistory", "Muse inline history has no items"); + for (const item of response.history.items) await this.fold(item); + } + await this.restorePending(response.pendingRequests); + // Inline history precedes suffix events; replayed turn terminals do not own a fresh run. + this.opening = false; + for (const notification of this.buffered) this.enqueue(notification); + this.buffered.length = 0; + await this.notifications; + this.catalog = presentCatalog( + await this.host.request("model/list", { sessionId: this.live().nativeId }, catalogSchema), + ); + const selected = this.catalog.models.find((model) => model.id === this.config.model); + this.config = { + ...this.config, + thinkingOption: this.config.thinkingOption ?? selected?.defaultThinkingOptionId, + }; + await this.live().commands.refresh(); + this.publishPersistence(); + this.publishConfig(); + this.options.emit({ + type: "session.ready", + requestId: input.requestId, + sessionId: this.options.id, + }); + } + + private live(): Live { + if (!this.liveState) throw new MuseError("sessionNotOpen", "Muse session is not open"); + return this.liveState; + } + + private async restorePending( + pointers: z.infer["pendingRequests"], + ): Promise { + if (!pointers.some((pending) => ["approval", "userInput"].includes(pending.kind))) return; + const pending = await this.host.request( + "approval/listPending", + { sessionId: this.live().nativeId }, + pendingSchema, + ); + for (const approval of pending.approvals) await this.permission(approval); + for (const question of pending.userInputs) this.live().questions.requested(question); + } + async prompt(prompt: ProviderPrompt): Promise { + if (await this.live().commands.compact(prompt)) return; + const id = commandId(); + this.messages.set(id, prompt.clientMessageId); + const input: TurnStartParams["input"] = []; + const text: string[] = []; + if (this.firstTurn && this.config.systemPrompt) + input.push({ + type: "text", + text: `\n${this.config.systemPrompt}\n`, + }); + if (prompt.input.type === "command") { + input.push(this.live().commands.input(prompt.input)); + text.push( + `/${prompt.input.name}${prompt.input.arguments ? ` ${prompt.input.arguments}` : ""}`, + ); + } else + for (const part of prompt.input.content) { + if (part.type === "text") { + input.push({ type: "text", text: part.text }); + text.push(part.text); + } else if (part.type === "image") + input.push({ type: "image", base64Data: part.data, mediaType: part.mimeType }); + else throw new MuseError("unsupported", `Muse does not support ${part.type} attachments`); + } + const params: TurnStartParams = { + commandId: id, + sessionId: this.live().nativeId, + input, + displayText: text.join("\n"), + ifBusy: prompt.delivery === "steer" ? "steer" : "queue", + reasoningEffort: this.config.thinkingOption + ? effortSchema.parse(this.config.thinkingOption) + : undefined, + }; + const response = await this.host.command("turn/start", params, promptResultSchema); + this.firstTurn = false; + if (response.disposition === "started" && this.turns.get(response.turnId) !== "completed") + this.turns.set(response.turnId, "active"); + this.live().recovery.activity(undefined, this.activeTurnIds().length > 0); + const type = response.disposition === "steered" ? "steer" : "turn"; + this.options.emit({ + type: "session.prompt_result", + sessionId: this.options.id, + clientMessageId: prompt.clientMessageId, + result: { type, turnId: response.turnId }, + }); + } + async configure(changes: ProviderConfigChanges): Promise { + if (changes.model !== undefined) { + if (!changes.model) throw new MuseError("invalidModel", "Choose a Muse model"); + await this.host.command( + "session/setModel", + { + sessionId: this.live().nativeId, + model: { providerId: this.providerId, modelId: changes.model }, + }, + ackSchema, + ); + this.config = { ...this.config, model: changes.model }; + } + // Muse receives configured thinking effort on each turn/start, rather than a session mutation. + if (changes.thinkingOption !== undefined) { + const thinkingOption = + changes.thinkingOption === null ? undefined : effortSchema.parse(changes.thinkingOption); + this.config = { ...this.config, thinkingOption }; + } + if (changes.mode !== undefined) { + const mode = approvalModeSchema.parse(changes.mode ?? "onRequest"); + await this.host.command( + "session/setApprovalMode", + { sessionId: this.live().nativeId, mode }, + ackSchema, + ); + this.config = { ...this.config, mode }; + } + this.publishConfig(); + this.publishPersistence(); + } + async answer(permissionId: string, response: ProviderPermissionResponse): Promise { + if (await this.live().questions.answer(permissionId, response)) return; + const approval = this.approvals.get(permissionId); + if (!approval) + throw new MuseError("approvalNotFound", "This Muse approval is no longer pending"); + const choice = response.selectedActionId + ? approval.availableChoices.find( + (candidate) => candidate.choiceId === response.selectedActionId, + ) + : (approval.availableChoices.find((candidate) => + response.behavior === "allow" + ? candidate.decision === "approved" && candidate.scope === "once" + : candidate.decision === "denied" && candidate.scope === "once", + ) ?? + (response.behavior === "deny" + ? approval.availableChoices.find( + (candidate) => candidate.decision === "abort" && candidate.scope === "once", + ) + : undefined)); + if (!choice) + throw new MuseError( + "invalidApprovalChoice", + "Choose one of Muse's available approval actions", + ); + await this.decide(approval, choice.choiceId); + } + private async decide(approval: WireApproval, choiceId: string): Promise { + await this.host.command( + "approval/decide", + { + sessionId: this.live().nativeId, + approvalId: approval.approvalId, + choiceId, + requirementId: approval.currentRequirementId, + }, + ackSchema, + ); + } + async readUsage() { + return this.host.request("usage/read", {}, usageSchema); + } + async interrupt(): Promise { + const turnId = this.activeTurnIds().at(-1); + if (!turnId) return; + let timer: ReturnType; + const completion = new Promise((resolve, reject) => { + this.interruptCompletion = { turnId, resolve, reject }; + timer = setTimeout(() => { + this.runtimeFailed( + new MuseError( + "interruptTimeout", + "Muse did not confirm cancellation within 10000ms. Reopen the session before sending another prompt.", + ), + ); + void this.host.close(); + }, 10000); + }); + try { + await Promise.all([ + this.host.command("turn/interrupt", { sessionId: this.live().nativeId, turnId }, ackSchema), + completion, + ]); + } finally { + clearTimeout(timer!); + this.interruptCompletion = null; + } + } + async close(): Promise { + this.closing = true; + if (this.liveState) this.live().recovery.close(); + await this.host.close(); + await this.notifications; + if (this.liveState) this.live().children.close(); + } + private enqueue(notification: Notification): void { + this.notifications = this.notifications + .then(() => this.notify(notification)) + .catch((error: unknown) => this.runtimeFailed(error)); + } + private runtimeFailed(error: unknown): void { + this.live().recovery.close(); + this.options.emit({ + type: "session.runtime_failed", + sessionId: this.options.id, + error: actionableError(error, this.options.launch), + }); + const interrupt = this.interruptCompletion; + if (interrupt) interrupt.reject(error instanceof Error ? error : new Error(String(error))); + } + private async notify(notification: Notification): Promise { + const envelope = notificationSchema.parse(notification.params); + if (envelope.sessionId && envelope.sessionId !== this.live().nativeId) { + await this.live().children.notify(envelope.sessionId, notification); + return; + } + await this.processNotification(notification); + this.live().recovery.activity(envelope.viewCursor, this.activeTurnIds().length > 0); + if (envelope.viewCursor) { + this.cursor = envelope.viewCursor; + this.publishPersistence(); + } + } + private async processNotification(notification: Notification): Promise { + switch (notification.method) { + case "view/gap": + // The gap lower bound remains authoritative even if a later live event arrived first. + await this.live().recovery.backfill(gapSchema.parse(notification.params).after); + break; + case "userInput/requested": + this.live().questions.requested(notification.params); + break; + case "userInput/settled": + this.live().questions.resolved(notification.params); + break; + case "session/todoListChanged": + this.options.emit({ + type: "timeline.item", + sessionId: this.options.id, + item: todoItem(notification.params), + }); + break; + case "skill/changed": + await this.live().commands.refresh(); + break; + case "item/started": + case "item/updated": + case "item/completed": { + const params = itemNotificationSchema.parse(notification.params); + await this.fold(params.item); + break; + } + case "item/delta": + this.live().timeline.delta(deltaSchema.parse(notification.params)); + break; + case "approval/requested": + case "approval/updated": + await this.permission(approvalSchema.parse(notification.params)); + break; + case "approval/resolved": { + const params = approvalResolvedSchema.parse(notification.params); + this.approvals.delete(params.approvalId); + this.options.emit({ + type: "session.permission_resolved", + sessionId: this.options.id, + permissionId: params.approvalId, + }); + break; + } + case "turn/started": + this.startTurn(notification.params); + break; + case "turn/completed": + this.completeTurn(notification.params); + break; + case "session/tokenUsage": { + const params = tokenUsageSchema.parse(notification.params); + this.options.emit({ + type: "session.usage", + sessionId: this.options.id, + usage: { + inputTokens: params.cumulative.promptTokens, + outputTokens: params.cumulative.outputTokens, + cachedInputTokens: params.usage?.cachedTokens, + }, + }); + break; + } + case "session/contextUsage": { + const params = contextUsageSchema.parse(notification.params); + this.options.emit({ + type: "session.usage", + sessionId: this.options.id, + usage: { + contextWindowUsedTokens: params.usedTokens, + contextWindowMaxTokens: params.windowTokens, + }, + }); + break; + } + } + } + private startTurn(input: unknown): void { + const params = turnSchema.parse(input); + if ( + params.commandId && + this.messages.has(params.commandId) && + this.turns.get(params.turnId) !== "completed" + ) { + this.turns.set(params.turnId, "active"); + this.options.emit({ + type: "session.turn", + sessionId: this.options.id, + turnId: params.turnId, + state: "started", + }); + } + } + private activeTurnIds(): string[] { + return [...this.turns].filter(([, state]) => state === "active").map(([id]) => id); + } + private completeTurn(input: unknown): void { + const params = turnSchema.parse(input); + if (this.turns.get(params.turnId) !== "active") return; + this.turns.set(params.turnId, "completed"); + let state: "completed" | "canceled" | "failed" = "completed"; + let error; + if (params.terminal === "cancelled") state = "canceled"; + else if (params.error || params.terminal === "failed") { + state = "failed"; + error = params.error + ? actionableError( + new MuseError(params.error.kind, params.error.message), + this.options.launch, + ) + : { message: "Muse turn failed" }; + } + this.options.emit({ + type: "session.turn", + sessionId: this.options.id, + turnId: params.turnId, + state, + error, + }); + if (this.interruptCompletion?.turnId === params.turnId) this.interruptCompletion.resolve(); + } + + private async fold(item: WireItem): Promise { + if (await this.live().timeline.fold(item)) await this.live().children.update(item); + } + private async permission(approval: WireApproval): Promise { + const current = approval.subject.stages.find( + (stage) => stage.requirementId.sourceIndex === approval.currentRequirementId?.sourceIndex, + ); + if (current && current.resolution.kind !== "unresolved") return; + this.approvals.set(approval.approvalId, approval); + if (this.config.mode === "allowAll") { + const choice = approval.availableChoices.find( + (candidate) => candidate.decision === "approved" && candidate.scope === "once", + ); + if (!choice) + throw new MuseError("noAllowOnceChoice", "Muse escalation has no allow-once action"); + await this.decide(approval, choice.choiceId); + return; + } + let title = approval.toolName; + if (current) title = `Stage ${current.position} of ${current.totalStages}`; + let detail: ProviderToolCallDetail; + const subject = approval.subject; + if (subject.kind === "shell") + detail = { + type: "shell", + command: current ? current.argv.join(" ") : subject.command, + cwd: subject.workspaceRoot, + }; + else if (subject.kind === "fileAccess") detail = { type: "edit", filePath: subject.path }; + else + detail = { + type: "plain_text", + label: subject.kind, + text: subject.host || subject.path || subject.command, + }; + const actions = approval.availableChoices.map((choice) => { + const behavior = choice.decision.startsWith("approved") ? "allow" : "deny"; + return { id: choice.choiceId, label: choice.label, behavior } as const; + }); + this.options.emit({ + type: "session.permission", + sessionId: this.options.id, + request: { + id: approval.approvalId, + name: approval.toolName, + kind: "tool", + title, + detail, + actions, + }, + }); + } + private persistence(): ProviderPersistence { + return { + version: 1, + data: { + sessionId: this.live().nativeId, + ...(this.cursor ? { cursor: this.cursor } : {}), + ...(this.config.model ? { model: this.config.model } : {}), + ...(this.config.thinkingOption ? { thinkingOption: this.config.thinkingOption } : {}), + }, + }; + } + private publishPersistence(): void { + this.options.emit({ + type: "session.persistence", + sessionId: this.options.id, + persistence: this.persistence(), + }); + } + private publishConfig(): void { + this.options.emit({ + type: "session.config", + sessionId: this.options.id, + config: { + model: this.config.model, + mode: this.config.mode, + thinkingOption: this.config.thinkingOption, + models: this.catalog.models, + modes, + thinkingOptions: + this.catalog.models.find((model) => model.id === this.config.model)?.thinkingOptions ?? + efforts.map((id) => ({ id, label: id })), + settings: [], + }, + }); + } +} +function mcpServer(server: ProviderMcpServerConfig): SessionMcpServerConfig { + if (server.type === "stdio") + return { + transport: "stdio", + command: server.command, + args: server.args, + env: server.env, + framing: "lineDelimitedJson", + }; + return { transport: "streamableHttp", url: server.url, headers: server.headers }; +} diff --git a/plugins/muse-provider/server/sessions.ts b/plugins/muse-provider/server/sessions.ts new file mode 100644 index 0000000000..93a22cd54b --- /dev/null +++ b/plugins/muse-provider/server/sessions.ts @@ -0,0 +1,57 @@ +import type { z } from "zod"; +import type { + ProviderInput, + ProviderLaunch, + ProviderSessionSummary, +} from "@getpaseo/plugin/server/provider"; +import { MspConnection } from "./connection.js"; +import { sessionListSchema } from "./wire.js"; + +export class Sessions { + private readonly hosts = new Set(); + async list( + launch: ProviderLaunch, + input: Extract, + ): Promise { + const host = new MspConnection({ launch }); + this.hosts.add(host); + try { + await host.initialize(); + const summaries: ProviderSessionSummary[] = []; + let cursor: string | null = null; + do { + const page: z.infer = await host.request( + "session/list", + { workspaceRoot: input.cwd, limit: Math.min(input.limit ?? 50, 200), cursor }, + sessionListSchema, + ); + for (const session of page.sessions) { + if (session.workspaceRoot === null) continue; + if ( + input.query && + !(session.title ?? "").toLowerCase().includes(input.query.toLowerCase()) + ) + continue; + summaries.push({ + cwd: session.workspaceRoot, + title: session.title, + updatedAt: session.updatedAt, + persistence: { + version: 1, + data: { sessionId: session.sessionId, model: session.modelId }, + }, + }); + if (input.limit && summaries.length >= input.limit) return summaries; + } + cursor = page.nextCursor; + } while (cursor !== null); + return summaries; + } finally { + await host.close(); + this.hosts.delete(host); + } + } + async close(): Promise { + await Promise.all([...this.hosts].map((host) => host.close())); + } +} diff --git a/plugins/muse-provider/server/settings.ts b/plugins/muse-provider/server/settings.ts new file mode 100644 index 0000000000..21586834ce --- /dev/null +++ b/plugins/muse-provider/server/settings.ts @@ -0,0 +1,17 @@ +import type { PluginSettings } from "@getpaseo/plugin/server"; +import { settingsSchema } from "../shared/settings.js"; +import { MuseError } from "./errors.js"; + +export async function serveArgs( + settings: PluginSettings, +): Promise { + const state = await settings.read(); + if (state.status === "invalid") throw new MuseError("invalidSettings", state.error); + const { sandbox, network, trustWorkspace } = state.values; + return [ + "--sandbox-network", + network, + ...(sandbox ? [] : ["--disable-sandbox"]), + ...(trustWorkspace ? ["--trust-workspace"] : []), + ]; +} diff --git a/plugins/muse-provider/server/timeline.ts b/plugins/muse-provider/server/timeline.ts new file mode 100644 index 0000000000..df786df125 --- /dev/null +++ b/plugins/muse-provider/server/timeline.ts @@ -0,0 +1,234 @@ +import { MspConnection } from "./connection.js"; +import { MuseError } from "./errors.js"; +import { todoSchema, outputSchema } from "./wire.js"; +import type { ProviderEvent } from "@getpaseo/plugin/server/provider"; +import type { z } from "zod"; +import type { + ProviderTimelineItem, + ProviderToolCallDetail, +} from "@getpaseo/plugin/server/provider"; +import { patchSchema, toolArgsSchema, deltaSchema, type WireItem } from "./wire.js"; + +export function unifiedDiff(content: string): string { + const patch = patchSchema.parse(JSON.parse(content)); + return patch.files + .map((file) => { + const hunks = file.hunks.map( + (hunk) => + `@@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@\n${hunk.lines.join("\n")}\n`, + ); + return `--- a/${file.path}\n+++ b/${file.path}\n${hunks.join("")}`; + }) + .join(""); +} + +function toolDetail(item: WireItem, patch?: string): ProviderToolCallDetail { + const parsed = toolArgsSchema.safeParse(parseArguments(item.args)); + if (!parsed.success) return { type: "unknown", input: item.args, output: item.visibleOutput }; + const args = parsed.data; + switch (item.tool) { + case "workflow": + case "subagent_spawn": + case "subagent_wait": + case "subagent_read_result": + return subAgentDetail(item, args); + case "shell": + case "bash": + return { + type: "shell", + command: item.commandText ?? args.command, + output: item.visibleOutput, + exitCode: item.exitCode, + }; + case "read_file": + return { type: "read", filePath: args.path, content: item.visibleOutput }; + case "edit_file": + return { + type: "edit", + filePath: args.path, + oldString: args.find, + newString: args.replace, + unifiedDiff: patch, + }; + case "write_file": + return { type: "write", filePath: args.path, content: args.content }; + case "grep": + case "glob": + case "ls": + case "web_search": + return { + type: "search", + query: args.query || args.pattern || args.path, + content: item.visibleOutput, + }; + case "web_fetch": + return { type: "fetch", url: args.url, result: item.visibleOutput }; + default: + return { type: "unknown", input: item.args, output: item.visibleOutput }; + } +} +function subAgentDetail( + item: WireItem, + args: z.infer, +): ProviderToolCallDetail { + return { + type: "sub_agent", + description: item.objective ?? item.entryId ?? args.objective, + subAgentType: item.role ?? args.role, + childSessionId: item.childSessionId, + log: item.message || item.visibleOutput || item.fallbackText, + }; +} + +function parseArguments(args: string): unknown { + try { + return JSON.parse(args); + } catch { + return args; + } +} + +export function timelineItem( + item: WireItem, + clientMessageId?: string, + patch?: string, +): ProviderTimelineItem { + const id = item.itemId; + switch (item.kind) { + case "userMessage": + return { type: "user_message", id, text: item.displayText ?? item.text, clientMessageId }; + case "agentMessage": + return { type: "assistant_message", id, text: item.text }; + case "reasoning": + return { type: "reasoning", id, text: item.summary.join("\n") }; + case "compaction": + return { + type: "compaction", + id, + status: item.status === "inProgress" ? "loading" : "completed", + preTokens: item.tokensBefore, + }; + case "workflow": + case "subagent": + case "toolCall": { + return toolCallItem(item, patch); + } + default: + return { + type: "tool_call", + id, + callId: id, + name: item.kind, + status: "completed", + error: null, + detail: { type: "plain_text", label: item.kind, text: item.fallbackText }, + }; + } +} + +function toolCallItem(item: WireItem, patch?: string): ProviderTimelineItem { + const id = item.itemId; + const base = { + type: "tool_call", + id, + callId: item.callId ?? id, + name: item.kind === "toolCall" ? item.tool : item.kind, + detail: + item.kind === "subagent" || item.kind === "workflow" + ? { + type: "sub_agent" as const, + subAgentType: item.role, + description: item.objective ?? item.entryId, + childSessionId: item.childSessionId, + log: item.message || item.visibleOutput || item.fallbackText, + } + : toolDetail(item, patch), + } as const; + if (item.status === "inProgress") return { ...base, status: "running", error: null }; + if (item.status === "cancelled") return { ...base, status: "canceled", error: null }; + if (item.status !== "completed") + return { ...base, status: "failed", error: item.failureReason ?? item.status }; + return { ...base, status: "completed", error: null }; +} + +export function appendDelta( + previous: WireItem, + delta: z.infer, +): WireItem | null { + if (delta.field === "text") return { ...previous, text: previous.text + delta.delta }; + if (delta.field === "output") + return { ...previous, visibleOutput: previous.visibleOutput + delta.delta }; + const match = /^summary\.(\d+)$/.exec(delta.field); + if (!match) return null; + const index = Number(match[1]); + const summary = [...previous.summary]; + summary[index] = (summary[index] ?? "") + delta.delta; + return { ...previous, summary }; +} + +export class Timeline { + private readonly items = new Map(); + constructor( + private readonly host: MspConnection, + private readonly nativeId: string, + private readonly id: string, + private readonly emit: (event: ProviderEvent) => void, + private readonly clientMessage: (commandId: string) => string | undefined = () => undefined, + ) {} + async fold(item: WireItem): Promise { + // Reminder children are Muse housekeeping, not agent work or navigable subagents. + if (item.kind === "reminderChild") return false; + const previous = this.items.get(item.itemId); + if (previous && previous.revision >= item.revision) return false; + let patch; + if (item.patchRef && item.status === "completed") { + const output = await this.host.request( + "item/readOutput", + { sessionId: this.nativeId, itemId: item.itemId, outputRef: item.patchRef.id }, + outputSchema, + ); + if (output.encoding !== "utf8" || !output.eof) + throw new MuseError("invalidPatch", "Muse edit patch is not a complete UTF-8 document"); + patch = unifiedDiff(output.content); + } + this.items.set(item.itemId, item); + this.emit({ + type: "timeline.item", + sessionId: this.id, + item: timelineItem( + item, + item.commandId ? this.clientMessage(item.commandId) : undefined, + patch, + ), + timestamp: item.recordedAt, + }); + return true; + } + delta(delta: z.infer): void { + const previous = this.items.get(delta.itemId); + if (!previous || previous.status !== "inProgress") return; + const item = appendDelta(previous, delta); + if (!item) return; + this.items.set(item.itemId, item); + this.emit({ type: "timeline.item", sessionId: this.id, item: timelineItem(item) }); + } +} + +export function todoItem(input: unknown): ProviderTimelineItem { + const todo = todoSchema.parse(input); + return { + type: "todo", + id: `todo:${todo.viewCursor}`, + items: todo.items.map((item) => { + let status: "pending" | "in_progress" | "completed" = "pending"; + if (item.status === "inProgress") status = "in_progress"; + else if (item.status === "completed") status = "completed"; + return { + text: item.text, + completed: item.status === "completed", + status, + activeForm: item.activeForm, + }; + }), + }; +} diff --git a/plugins/muse-provider/server/usage.ts b/plugins/muse-provider/server/usage.ts new file mode 100644 index 0000000000..a60b0a34b2 --- /dev/null +++ b/plugins/muse-provider/server/usage.ts @@ -0,0 +1,111 @@ +import { homedir } from "node:os"; +import path from "node:path"; +import { z } from "zod"; +import type { UsageSourceRegistration } from "@getpaseo/plugin/server"; +import type { ProviderLaunch } from "@getpaseo/plugin/server/provider"; +import { + hashAccountKey, + unavailableUsage, + windowFromUsedPct, + toneFromUsedPct, +} from "@getpaseo/plugin/server/usage"; + +import { MspConnection } from "./connection.js"; +import { usageSchema } from "./wire.js"; + +const inputSchema = z.object({ account: z.string() }).strict(); +type Observation = z.infer; +export class Usage { + private readonly launches = new Map(); + private readonly sessions = new Map< + string, + { account: string; read: () => Promise } + >(); + remember(launch: ProviderLaunch): { account: string } { + // MSP 1.4.1 has no stable account ID; swapping accounts in one config directory shares a report ID. + const configHome = + launch.env.XDG_CONFIG_HOME ?? path.join(launch.env.HOME ?? homedir(), ".config"); + const key = hashAccountKey(path.resolve(configHome, "muse")); + this.launches.set(key, launch); + return { account: key }; + } + attach(id: string, launch: ProviderLaunch, read: () => Promise): void { + this.sessions.set(id, { ...this.remember(launch), read }); + } + detach(id: string): void { + this.sessions.delete(id); + } + private async read(account: string, launch: ProviderLaunch): Promise { + const readers = [...this.sessions.values()].filter((session) => session.account === account); + if (readers.length > 0) { + const observations = await Promise.all(readers.map((session) => session.read())); + const usages = observations.flatMap((observation) => + observation.usage ? [observation.usage] : [], + ); + usages.sort((a, b) => b.observedAtMs - a.observedAtMs); + return usages.length > 0 ? { usage: usages[0] } : {}; + } + const host = new MspConnection({ launch }); + try { + await host.initialize(); + return await host.request("usage/read", {}, usageSchema); + } finally { + await host.close(); + } + } + registration(): UsageSourceRegistration { + return { + id: "muse", + label: "Muse Code", + icon: "icon.svg", + input: inputSchema, + discover: async () => [...this.launches.keys()].map((account) => ({ account })), + identify: async (input) => { + const { account } = inputSchema.parse(input); + return this.launches.has(account) ? { key: account, label: "Muse Code" } : null; + }, + fetch: async (input) => { + const { account } = inputSchema.parse(input); + const launch = this.launches.get(account); + if (!launch) return unavailableUsage(); + try { + const { usage } = await this.read(account, launch); + if (!usage) return unavailableUsage(); + return { + status: "available", + planLabel: usage.tier, + windows: [ + windowFromUsedPct({ + id: "five_hour", + label: `${usage.window.windowDurationMins / 60} hours`, + utilizationPct: usage.window.usedPercent, + resetsAt: new Date(usage.window.resetsAtMs).toISOString(), + tone: toneFromUsedPct(usage.window.usedPercent), + }), + windowFromUsedPct({ + id: "weekly", + label: "Weekly", + utilizationPct: usage.weekly.usedPercent, + resetsAt: new Date(usage.weekly.resetsAtMs).toISOString(), + tone: toneFromUsedPct(usage.weekly.usedPercent), + }), + ], + details: [ + { + id: "observed", + label: "Observed", + value: new Date(usage.observedAtMs).toISOString(), + }, + ], + }; + } catch (error) { + return { + status: "error", + windows: [], + error: error instanceof Error ? error.message : String(error), + }; + } + }, + }; + } +} diff --git a/plugins/muse-provider/server/wire.ts b/plugins/muse-provider/server/wire.ts new file mode 100644 index 0000000000..4955acdaa5 --- /dev/null +++ b/plugins/muse-provider/server/wire.ts @@ -0,0 +1,278 @@ +import { z } from "zod"; + +export const fingerprint = + "sha256:e0e163db6ccf00dbe68402ce55d6319b3edc33c421f31e9583b587b2de8a118f"; +export const effortSchema = z.enum([ + "none", + "minimal", + "low", + "medium", + "high", + "xhigh", + "max", + "ultra", +]); +export const approvalModeSchema = z.enum([ + "onRequest", + "promptUnmatched", + "denyUnmatched", + "allowAll", +]); +export const itemSchema = z.object({ + itemId: z.string(), + revision: z.number().int(), + childSessionId: z.string().optional(), + role: z.string().optional(), + objective: z.string().optional(), + message: z.string().default(""), + entryId: z.string().optional(), + kind: z.string(), + status: z.string(), + turnId: z.string().nullable().optional(), + commandId: z.string().optional(), + recordedAt: z.string().optional(), + text: z.string().default(""), + displayText: z.string().optional(), + summary: z.array(z.string()).default([]), + tool: z.string().default(""), + args: z.string().default("{}"), + callId: z.string().optional(), + commandText: z.string().optional(), + visibleOutput: z.string().default(""), + exitCode: z.number().optional(), + fallbackText: z.string().default(""), + failureReason: z.string().optional(), + patchRef: z.object({ id: z.string() }).optional(), + tokensBefore: z.number().optional(), +}); +export type WireItem = z.infer; +export const requirementSchema = z.object({ + approvalId: z.string(), + sourceIndex: z.number().optional(), +}); +export const choiceSchema = z.object({ + choiceId: z.string(), + decision: z.string(), + label: z.string(), + scope: z.string(), +}); +export const approvalSchema = z.object({ + approvalId: z.string(), + currentRequirementId: requirementSchema.optional(), + availableChoices: z.array(choiceSchema), + toolName: z.string().default("Tool"), + subject: z.object({ + kind: z.string(), + command: z.string().default(""), + path: z.string().default(""), + host: z.string().default(""), + workspaceRoot: z.string().optional(), + stages: z + .array( + z.object({ + position: z.number(), + totalStages: z.number(), + argv: z.array(z.string()), + requirementId: requirementSchema, + resolution: z.object({ kind: z.string() }), + }), + ) + .default([]), + }), +}); +export type WireApproval = z.infer; +export const notificationSchema = z.object({ + sessionId: z.string().optional(), + viewCursor: z.string().optional(), +}); +export const frameSchema = z.object({ + jsonrpc: z.literal("2.0"), + id: z.union([z.string(), z.number()]).optional(), + method: z.string().optional(), + params: z.unknown().optional(), + result: z.unknown().optional(), + error: z + .object({ + code: z.number(), + message: z.string(), + data: z + .object({ kind: z.string().optional(), reason: z.string().optional() }) + .passthrough() + .optional(), + }) + .optional(), +}); +export const initializedSchema = z.object({ schema: z.object({ fingerprint: z.string() }) }); +export const accountSchema = z.object({ + state: z.string(), +}); +export const catalogSchema = z.object({ + models: z.array( + z.object({ + modelId: z.string(), + providerId: z.string(), + displayLabel: z.string(), + contextLimit: z.number().nullable(), + isDefault: z.boolean(), + defaultReasoningEffort: effortSchema.nullable().optional(), + reasoningEffortVariants: z.union([z.array(effortSchema), z.literal("unknown")]).default([]), + variants: z.array(effortSchema).default([]), + }), + ), +}); +export const sessionSchema = z.object({ + session: z.object({ + sessionId: z.string(), + modelId: z.string(), + providerId: z.string(), + approvalMode: z.object({ mode: approvalModeSchema }).optional(), + }), + viewCursor: z.string(), + history: z + .object({ mode: z.string(), items: z.array(itemSchema).nullable().default([]) }) + .optional(), + pendingRequests: z + .array( + z.object({ kind: z.string(), approvalId: z.string().optional(), viewCursor: z.string() }), + ) + .default([]), +}); +export const persistenceSchema = z.object({ + sessionId: z.string(), + cursor: z.string().optional(), + model: z.string().optional(), + thinkingOption: effortSchema.optional(), +}); +export const deltaSchema = z.object({ itemId: z.string(), field: z.string(), delta: z.string() }); +export const turnSchema = z.object({ + turnId: z.string(), + commandId: z.string().optional(), + terminal: z.string().optional(), + error: z.object({ kind: z.string(), message: z.string() }).optional(), +}); +export const promptResultSchema = z.object({ turnId: z.string(), disposition: z.string() }); +export const outputSchema = z.object({ + content: z.string(), + encoding: z.enum(["utf8", "base64"]), + eof: z.boolean(), +}); +export const patchSchema = z.object({ + files: z.array( + z.object({ + path: z.string(), + hunks: z.array( + z.object({ + oldStart: z.number(), + oldLines: z.number(), + newStart: z.number(), + newLines: z.number(), + lines: z.array(z.string()), + }), + ), + }), + ), +}); +export const toolArgsSchema = z.object({ + objective: z.string().optional(), + role: z.string().optional(), + command: z.string().default(""), + path: z.string().default(""), + find: z.string().optional(), + replace: z.string().optional(), + content: z.string().optional(), + query: z.string().default(""), + pattern: z.string().default(""), + url: z.string().default(""), +}); +export const tokenUsageSchema = z.object({ + cumulative: z.object({ promptTokens: z.number(), outputTokens: z.number() }), + usage: z.object({ cachedTokens: z.number().optional() }).optional(), +}); +export const contextUsageSchema = z.object({ + usedTokens: z.number(), + windowTokens: z.number().optional(), +}); + +export const ackSchema = z.object({}).passthrough(); +export const skillsSchema = z.object({ + skills: z.array( + z.object({ + selector: z.string(), + description: z.string(), + argumentHint: z.string().optional(), + }), + ), +}); +export const questionSchema = z.object({ + userInputId: z.string(), + toolName: z.string(), + questions: z.array( + z.object({ + id: z.string(), + header: z.string(), + question: z.string(), + options: z.array(z.object({ label: z.string(), description: z.string().default("") })), + selection: z.object({ + mode: z.enum(["single", "multiple"]), + minSelections: z.number().optional(), + maxSelections: z.number().optional(), + }), + }), + ), +}); +export const settledQuestionSchema = z.object({ userInputId: z.string() }); +export const questionAnswersSchema = z.object({ answers: z.record(z.string(), z.string()) }); +export const todoSchema = z.object({ + viewCursor: z.string(), + items: z.array( + z.object({ text: z.string(), status: z.string(), activeForm: z.string().optional() }), + ), +}); +export const pageSchema = z.object({ + events: z.array(z.object({ method: z.string(), params: z.record(z.string(), z.unknown()) })), + nextCursor: z.string().nullable(), +}); +export const gapSchema = z.object({ after: z.string(), next: z.string() }); +export const sessionListSchema = z.object({ + nextCursor: z.string().nullable(), + sessions: z.array( + z.object({ + sessionId: z.string(), + workspaceRoot: z.string().nullable(), + title: z.string().optional(), + updatedAt: z.string(), + modelId: z.string(), + }), + ), +}); +export const usageSchema = z.object({ + usage: z + .object({ + observedAtMs: z.number(), + tier: z.string(), + window: z.object({ + resetsAtMs: z.number(), + usedPercent: z.number(), + windowDurationMins: z.number(), + }), + weekly: z.object({ resetsAtMs: z.number(), usedPercent: z.number() }), + }) + .optional(), +}); +export const childSessionSchema = z.object({ + session: z.object({ + sessionId: z.string(), + workspaceRoot: z.string().nullable(), + title: z.string().optional(), + }), + viewCursor: z.string(), +}); + +export const itemNotificationSchema = z.object({ item: itemSchema }); + +export const pendingSchema = z.object({ + approvals: z.array(approvalSchema), + userInputs: z.array(questionSchema).default([]), +}); + +export const approvalResolvedSchema = z.object({ approvalId: z.string() }); diff --git a/plugins/muse-provider/shared/settings.ts b/plugins/muse-provider/shared/settings.ts new file mode 100644 index 0000000000..f1af368e8c --- /dev/null +++ b/plugins/muse-provider/shared/settings.ts @@ -0,0 +1,24 @@ +import { z } from "zod"; +import { defineSettings } from "@getpaseo/plugin"; + +export const settingsSchema = z.object({ + sandbox: z + .boolean() + .default(true) + .describe("Keep Muse filesystem and network sandboxing enabled"), + network: z + .enum(["proxy-only", "restricted", "enabled"]) + .default("proxy-only") + .describe("Sandbox network access; disabling sandbox forces full network access"), + trustWorkspace: z + .boolean() + .default(false) + .describe("Let Muse load project-scoped skills and config from the workspace"), +}); + +export const museSettings = defineSettings({ + id: "muse", + scope: "host", + version: 1, + schema: settingsSchema, +}); diff --git a/plugins/muse-provider/test/fake-muse.mjs b/plugins/muse-provider/test/fake-muse.mjs new file mode 100644 index 0000000000..fffca0cc89 --- /dev/null +++ b/plugins/muse-provider/test/fake-muse.mjs @@ -0,0 +1,404 @@ +import { appendFileSync, readFileSync } from "node:fs"; +import { createInterface } from "node:readline"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = path.join(path.dirname(fileURLToPath(import.meta.url)), "fixtures"); +const scenario = process.env.MUSE_TEST_SCENARIO || "text-reasoning"; +if (process.argv.includes("--version")) { + process.stdout.write(`Muse Code ${process.env.MUSE_TEST_VERSION || "1.4.1"}\n`); + process.exit(0); +} +const rows = readFixture(scenario); +if (process.env.MUSE_TEST_REQUESTS) + appendFileSync( + process.env.MUSE_TEST_REQUESTS, + JSON.stringify({ event: "hostStarted", args: process.argv.slice(2) }) + "\n", + ); +const catalogRows = readFixture("catalog-controls"); +const replacements = new Map(); +const counts = new Map(); +const receipts = new Set(); +const lines = createInterface({ input: process.stdin }); +lines.on("line", receive); +function receive(line) { + const frame = JSON.parse(line); + if (process.env.MUSE_TEST_REQUESTS) + appendFileSync(process.env.MUSE_TEST_REQUESTS, JSON.stringify(frame) + "\n"); + if (frame.result) { + receipts.delete(frame.id); + return; + } + if (controlResponse(frame)) return; + if (queuedResponse(frame)) return; + const candidates = rows.filter((row) => row.dir === "out" && row.msg.method === frame.method); + const count = counts.get(frame.method) || 0; + counts.set(frame.method, count + 1); + if (retryResponse(frame, count)) return; + const request = candidates[count]; + if (!request) + return rpcError( + frame, + -32601, + "unknownMethod", + `No fixture request for ${frame.method} #${count}`, + ); + const recorded = request.msg; + validateCommand(frame, recorded); + const response = rows.find( + (row) => row.dir === "in" && row.msg.id === recorded.id && !row.msg.method, + ); + if (!response) throw new Error(`Fixture response absent: ${frame.method}`); + const position = rows.indexOf(request); + let end = position + 1; + while (end < rows.length && !(rows[end].dir === "out" && rows[end].msg.method)) end++; + const messages = rows.slice(position + 1, end).filter((row) => row.dir === "in"); + if (!messages.includes(response)) messages.unshift(response); + if (process.env.MUSE_TEST_ACK_LAST && frame.method === "turn/start") { + const terminalFirst = messages.filter((row) => row !== response); + for (const row of terminalFirst) emitFixtureMessage(row.msg, recorded, frame); + setTimeout(() => emitFixtureMessage(response.msg, recorded, frame), 50); + } else for (const row of messages) emitFixtureMessage(row.msg, recorded, frame); +} +function controlResponse(frame) { + if (frame.method === "initialized") return true; + if (frame.method === "initialize" && process.env.MUSE_TEST_EXIT) { + process.stderr.write("diagnostic tail, not protocol\n"); + process.exit(Number(process.env.MUSE_TEST_EXIT)); + } + if (process.env.MUSE_TEST_HANG === frame.method) return true; + if (frame.method === "session/setReasoningEffort") { + respond(frame, { status: "accepted", commandId: frame.params.commandId }); + return true; + } + if (frame.method === "account/read") { + respond(frame, { credentialRequired: true, state: process.env.MUSE_TEST_ACCOUNT || "envKey" }); + return true; + } + if (parityResponse(frame)) return true; + if (frame.method === "model/list") { + const result = responseFor(catalogRows, "model/list"); + if (process.env.MUSE_TEST_MODELS) result.models = JSON.parse(process.env.MUSE_TEST_MODELS); + respond(frame, result); + return true; + } + if (frame.method === "session/resume" && process.env.MUSE_TEST_GONE) { + rpcError(frame, -32020, "sessionNotFound", "missing"); + return true; + } + return false; +} +function parityResponse(frame) { + if (workflowResponse(frame)) return true; + if (frame.method === "view/page") { + let result; + if (process.env.MUSE_TEST_GAP || process.env.MUSE_TEST_SILENT) { + result = { + events: replace( + rows + .filter( + (row) => + row.dir === "in" && + row.msg.method && + row.msg.params?.viewCursor && + !row.msg.id && + row.msg.method !== "item/delta", + ) + .map((row) => ({ method: row.msg.method, params: row.msg.params })), + ), + nextCursor: null, + }; + } else { + result = replace( + responseFor( + readFixture( + frame.params.sessionId === "fixture-child" ? "phase3-child-read" : "phase3-controls", + ), + "view/page", + ), + ); + for (const event of result.events) event.params.sessionId = frame.params.sessionId; + } + respond(frame, result); + return true; + } + if (frame.method === "session/read") { + const result = responseFor(readFixture("phase3-child-read"), "session/read"); + result.session.sessionId = frame.params.sessionId; + respond(frame, result); + return true; + } + if (frame.method === "usage/read") { + const result = process.env.MUSE_TEST_USAGE + ? JSON.parse(process.env.MUSE_TEST_USAGE) + : responseFor(catalogRows, "usage/read"); + respond(frame, result); + return true; + } + if (frame.method === "session/list") { + const result = responseFor(catalogRows, "session/list"); + result.nextCursor = null; + respond(frame, result); + return true; + } + if (frame.method === "userInput/cancel" && scenario !== "phase3-cancel") { + respond(frame, { + status: "accepted", + commandId: frame.params.commandId, + userInputId: frame.params.userInputId, + }); + send({ + jsonrpc: "2.0", + method: "userInput/settled", + params: { + sessionId: frame.params.sessionId, + userInputId: frame.params.userInputId, + outcome: "cancelled", + }, + }); + return true; + } + if (frame.method === "session/compact" && process.env.MUSE_TEST_COMPACT) { + respond(frame, { status: "accepted", commandId: frame.params.commandId }); + for (const [revision, status] of [ + [1, "inProgress"], + [2, "completed"], + ]) + send({ + jsonrpc: "2.0", + method: revision === 1 ? "item/started" : "item/completed", + params: { + sessionId: frame.params.sessionId, + item: { itemId: "fixture-compaction", revision, kind: "compaction", status }, + }, + }); + return true; + } + if (skillResponse(frame)) return true; + return false; +} +function skillResponse(frame) { + if (frame.method === "skill/list") { + const skillRows = + scenario.startsWith("phase3-") && rows.some((row) => row.msg.method === "skill/list") + ? rows + : readFixture("skill"); + const result = responseFor(skillRows, "skill/list"); + if (process.env.MUSE_TEST_SKILLS_CHANGED && counts.has("skill-read")) result.skills = []; + counts.set("skill-read", 1); + respond(frame, result); + return true; + } + return false; +} + +function workflowResponse(frame) { + if (frame.method === "view/page" && process.env.MUSE_TEST_WORKFLOW) { + const result = responseFor(readFixture("phase3-default-workflow"), "view/page"); + result.events = result.events.filter( + (event) => event.params.item?.kind === "workflow" || event.params.item?.tool === "workflow", + ); + for (const event of result.events) event.params.sessionId = frame.params.sessionId; + respond(frame, result); + return true; + } + return false; +} + +function queuedResponse(frame) { + if ( + process.env.MUSE_TEST_QUEUE && + frame.method === "turn/start" && + counts.get(frame.method) === 1 + ) { + respond(frame, { + turnId: frame.params.commandId, + disposition: "queued", + startedNewTurn: false, + }); + return true; + } + return false; +} +function retryResponse(frame, count) { + if (process.env.MUSE_TEST_RETRY === frame.method && count === 0) { + counts.set(frame.method, 0); + delete process.env.MUSE_TEST_RETRY; + replacements.set("retryCommandId", frame.params.commandId); + rpcError(frame, -32001, process.env.MUSE_TEST_RETRY_KIND || "overloaded", "busy"); + return true; + } + if ( + replacements.has("retryCommandId") && + replacements.get("retryCommandId") !== frame.params.commandId + ) + throw new Error("Retry changed commandId"); + replacements.delete("retryCommandId"); + return false; +} +function validateCommand(frame, recorded) { + if (recorded.params.commandId) { + if ( + !/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test( + frame.params.commandId, + ) + ) + throw new Error("Not UUIDv7"); + replacements.set(recorded.params.commandId, frame.params.commandId); + } + if ( + frame.method === "approval/decide" && + recorded.params.requirementId.sourceIndex !== frame.params.requirementId.sourceIndex + ) + throw new Error("Wrong approval stage"); +} +function emitFixtureMessage(recordedMessage, recorded, frame) { + const message = replace(recordedMessage); + if (!message.method && message.id === recorded.id) message.id = frame.id; + if ( + message.method === "session/closed" || + (message.method === "session/statusChanged" && message.params.status === "notLoaded") + ) + return; + if (frame.method === "session/start" && message.result?.session?.approvalMode) + message.result.session.approvalMode.mode = frame.params.approvalMode; + if (message.result?.viewCursor && process.env.MUSE_TEST_EMPTY_CURSOR) + message.result.viewCursor = ""; + applyTestVariants(message); + if (parityDelivery(message)) return; + if (message.method && message.id !== undefined) receipts.add(message.id); + send(message); + if ( + process.env.MUSE_TEST_STALE && + message.method === "item/completed" && + message.params.item.kind === "agentMessage" + ) { + send({ + ...message, + params: { + ...message.params, + item: { ...message.params.item, revision: 1, text: "STALE_REVISION" }, + }, + }); + } +} +function parityDelivery(message) { + if (process.env.MUSE_TEST_WORKFLOW && message.method === "turn/completed") + send({ + jsonrpc: "2.0", + method: "view/gap", + params: { sessionId: message.params.sessionId, after: "", next: message.params.viewCursor }, + }); + if ( + process.env.MUSE_TEST_SILENT && + ["item/started", "item/delta", "item/completed", "turn/completed"].includes(message.method) && + message.params.item?.kind !== "userMessage" + ) + return true; + if ( + process.env.MUSE_TEST_GAP && + message.method === "item/completed" && + message.params.item.kind === "agentMessage" + ) { + send({ + jsonrpc: "2.0", + method: "view/gap", + params: { + sessionId: message.params.sessionId, + after: "fixture-last-cursor", + next: message.params.viewCursor, + }, + }); + return true; + } + if (process.env.MUSE_TEST_SKILLS_CHANGED && message.method === "turn/completed") + send({ + jsonrpc: "2.0", + method: "skill/changed", + params: { sessionId: message.params.sessionId }, + }); + return false; +} +function applyTestVariants(message) { + applyParityVariants(message); + if (message.method === "turn/completed" && process.env.MUSE_TEST_INTERRUPT_TERMINAL) + message.params.terminal = process.env.MUSE_TEST_INTERRUPT_TERMINAL; + if (message.params?.availableChoices && process.env.MUSE_TEST_CHOICES) + message.params.availableChoices = message.params.availableChoices.toReversed(); + if (message.method === "turn/completed" && process.env.MUSE_TEST_AUTH_REQUIRED) { + message.params.terminal = "failed"; + message.params.error = { kind: "authRequired", message: "authentication expired" }; + } + if (message.result?.schema && process.env.MUSE_TEST_FINGERPRINT) + message.result.schema.fingerprint = process.env.MUSE_TEST_FINGERPRINT; + if (message.params?.item?.tool === "read_file" && process.env.MUSE_TEST_TOOL) { + message.params.item.tool = process.env.MUSE_TEST_TOOL; + message.params.item.args = JSON.stringify({ + path: "f", + query: "needle", + url: "https://example.com", + }); + } + if (message.params?.item?.tool === "read_file" && process.env.MUSE_TEST_UNKNOWN_KIND) { + message.params.item.kind = "futureItem"; + message.params.item.fallbackText = "New MSP item"; + } +} +function applyParityVariants(message) { + if (process.env.MUSE_TEST_CHILD && message.params?.item?.tool === "subagent_spawn") { + message.params.item.childSessionId = "fixture-child"; + if (process.env.MUSE_TEST_CHILD === "workflow") message.params.item.kind = "workflow"; + if (process.env.MUSE_TEST_CHILD === "dedicated") { + message.params.item.kind = "subagent"; + message.params.item.role = "worker"; + message.params.item.objective = "Child objective"; + } + } + if (message.params?.availableChoices && process.env.MUSE_TEST_DENIED) + message.params.availableChoices.push({ + choiceId: "reject_once", + decision: "denied", + scope: "once", + label: "Reject tool", + }); +} +lines.on("close", () => { + if (process.env.MUSE_TEST_REQUESTS) + appendFileSync(process.env.MUSE_TEST_REQUESTS, JSON.stringify({ event: "hostClosed" }) + "\n"); + if (receipts.size) throw new Error("Missing presentation receipt"); +}); +function readFixture(name) { + const transcript = readFileSync(path.join(root, name + ".ndjson"), "utf8") + .trim() + .split("\n") + .map((line) => JSON.parse(line)); + // The duplicate-decision probe is a separate negative action, not part of the approval flow. + const duplicateIds = new Set( + transcript + .filter((row) => row.msg.error?.data?.kind === "approvalAlreadyResolved") + .map((row) => row.msg.id), + ); + return transcript.filter((row) => !duplicateIds.has(row.msg.id)); +} +function responseFor(transcript, method) { + const request = transcript.find((row) => row.dir === "out" && row.msg.method === method); + return transcript.find( + (row) => row.dir === "in" && row.msg.id === request.msg.id && !row.msg.method, + ).msg.result; +} +function replace(value) { + if (typeof value === "string") return replacements.get(value) || value; + if (Array.isArray(value)) return value.map(replace); + if (value && typeof value === "object") + return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, replace(entry)])); + return value; +} +function send(frame) { + process.stdout.write(JSON.stringify(frame) + "\n"); +} +function respond(frame, result) { + send({ jsonrpc: "2.0", id: frame.id, result }); +} +function rpcError(frame, code, kind, message) { + send({ jsonrpc: "2.0", id: frame.id, error: { code, message, data: { kind } } }); +} diff --git a/plugins/muse-provider/test/fixtures/README.md b/plugins/muse-provider/test/fixtures/README.md new file mode 100644 index 0000000000..b294bf4a0b --- /dev/null +++ b/plugins/muse-provider/test/fixtures/README.md @@ -0,0 +1,78 @@ +# Muse MSP fixtures + +Captured on Linux on 2026-09-29 from Muse Code **1.4.1 (1.4.1-R4503.1)**, +build `35815c253477406321e3f5595195becc25e3907a`. + +- Stable handshake fingerprint: `sha256:e0e163db6ccf00dbe68402ce55d6319b3edc33c421f31e9583b587b2de8a118f`. +- Experimental export fingerprint: `sha256:b359d70b581b0ea471d35a7770d3039fdd35f19a46a2d42b43fd4b1674d52c8a`. +- Schema version: `1`. Both `sessionMcp` and `sessionListStream` were granted. + +Each line is `{ "dir": "out" | "in", "msg": }`. +`out` means client to host; `in` means host to client. IDs, timestamps, cursors, +revisions, deltas, and event order are retained. Replay consumers should treat IDs +and cursors as opaque and compare behavior rather than wall-clock values. +The skill catalogs retain only the test selectors `phase0` and `muse-parity-*`; unrelated personal, +bundled, and installed-plugin skill rows were removed. No credentials are included. + +A throwaway driver launched `muse serve` with isolated `XDG_CONFIG_HOME` and +`XDG_DATA_HOME`. Model turns used a custom Responses endpoint, an environment key, +`providerId: "meta"`, and `modelId: "meta/muse-spark-1.3"`. The settings pinned the +endpoint's `auth: "bearer"`, the default provider/model, and permission profile +`:ask-me` with its own `schema_version: 1`. The sandbox and default `proxy-only` +network policy remained enabled. Workspace trust was enabled for project skills. +The approval judge was disabled for staged approval probes so decisions reached +the client. The driver and local server remain outside the repository. + +| File | Recorded behavior | +| -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `text-reasoning.ndjson` | Session start, rational-sum prompt, public reasoning summary, streamed assistant text, token usage, terminal. | +| `tools-edit.ndjson` | Read `f`, run `wc -l f`, replace `beta` with `gamma`; fetch the JSON patch through `patchRef` and `item/readOutput`. | +| `staged-approval.ndjson` | `wc -l f && rm -rf f`: safe first stage, held second stage, receipt and decision, terminal resolution. Also records a duplicate decision rejected as already resolved. | +| `multi-stage-approval.ndjson` | `wc -l f && rm -rf f && rm -rf g`: successive decisions return `terminal: false`, then `true`; both files are removed. | +| `interrupt.ndjson` | Interrupt a running shell sleep; tool reconciliation and `turn/completed` with `terminal: "cancelled"`. | +| `steer.ndjson` | Submit `ifBusy: "steer"` during a shell sleep; accepted as `steered`, and the original turn replies with the replacement marker. | +| `resume-source.ndjson` | Complete a marker turn, then kill the host process before starting either resume probe. | +| `resume-without-cursor.ndjson` | New host returns inline history and runs a fresh turn recalling the marker. | +| `resume-with-cursor.ndjson` | New host returns `history.mode: "none"`, replays the cursor suffix, then completes a fresh marker turn. | +| `resume-reasoning.ndjson` | New host resumes the steering session, including its reasoning history, and completes another text turn. | +| `subagent.ndjson` | Native spawn/wait/read-result tool calls return a child result marker and an evidence log path. This build emits `toolCall`, without a dedicated `subagent` item. | +| `skill.ndjson` | Discover the project skill and invoke its selector through a structured skill input part. | +| `image-followup.ndjson` | Send a valid 16×16 PNG, then complete a text-only marker turn in the same session. Image-description accuracy is not asserted. | +| `catalog-controls.ndjson` | Model catalog, environment-key account state, empty subscription usage, paginated sessions, approval mode change, valid model selection, and rejection of a model outside the catalog. | +| `mcp-stdio.ndjson` | Admit a session-scoped stdio server. Model attempts fail with an unknown-tool routing error and no marker is returned; this is a failure fixture. | +| `auto-review-unavailable.ndjson` | Selecting `:auto-review` as the default profile rejects session start because the automated reviewer is unavailable. | +| `unsafe-path.ndjson` | Group-writable data directory rejects session start with `UnsafePath`. | +| `initialize-history.ndjson` | Handshake against 322 retained session logs (7,713,541 bytes); three measured launches took 155–160 ms. | + +The project skill body was “Reply with exactly SKILL_PHASE0_OK. Do not use tools.” +The injected stdio server implemented `initialize`, `tools/list`, and `tools/call`, +advertised `verification_echo` with an empty-object input schema, and returned +`MCP_PHASE0_OK` as text. The host registered `mcp__verification__verification_echo`, +but the model route attempted the unqualified `verification_echo`; the marker +was never delivered. Keep this distinction when testing MCP support. + +Phase 3 captures use the same build and isolated custom route. Sandbox and network +remain at their defaults. `phase3-user-skill` omits workspace trust; +`phase3-project-skill` enables it. Native subagent captures enable +`MUSE_EXPERIMENTAL_SDK_ENABLED=on`; the default workflow capture reads the session +from a daemon run that omitted the flag. Its workflow child IDs are internal +activity IDs, not MSP `childSessionId` values. + +| File | Recorded behavior | +| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| `phase3-controls.ndjson` | Two todo entries, a single-choice question, an answer and matching settlement, retained view paging, empty subscription usage. | +| `phase3-cancel.ndjson` | Question cancellation and matching cancelled settlement. | +| `phase3-child-read.ndjson` | Read and page an actual native child session, returning its assistant marker. | +| `phase3-default-workflow.ndjson` | Read and page the default daemon run: workflow revisions 1–7, child activity, and `MUSE_PARITY_CHILD_OK`; no `childSessionId`. | +| `phase3-user-skill.ndjson` | User-scoped skill catalog and structured invocation, returning `MUSE_PARITY_USER_OK` with trust off. | +| `phase3-project-skill.ndjson` | Project-scoped skill catalog and invocation, returning `MUSE_PARITY_PROJECT_OK` with trust on. | +| `phase3-compact.ndjson` | Resume a completed session; manual compaction rejects with `compaction_unavailable`. | +| `phase3-compact-history.ndjson` | Resume three completed turns; catalog context limit is null and compaction still rejects. | +| `phase3-compact-limit.ndjson` | Same session and route with only a configured context limit; compaction is admitted and completes, reducing 22,192 tokens to 21,322. | + +The subprocess fixture host also generates schema-based variants for dedicated +subagent items, explicit child-session linkage, nonempty subscription windows, +`skill/changed`, deny choices, lost events, watchdog silence, and late admission. +These variants test protocol behavior; they are not claimed as real-host captures. +The workflow replay triggers a generated gap to page the recorded default run. +Disk edits in a live host did not emit `skill/changed` during a 15-second probe. diff --git a/plugins/muse-provider/test/fixtures/auto-review-unavailable.ndjson b/plugins/muse-provider/test/fixtures/auto-review-unavailable.ndjson new file mode 100644 index 0000000000..3d6f6a8e83 --- /dev/null +++ b/plugins/muse-provider/test/fixtures/auto-review-unavailable.ndjson @@ -0,0 +1,5 @@ +{"dir":"out","msg":{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"clientInfo":{"name":"paseo_phase0","version":"0.0.0"},"capabilities":{"experimentalApi":true,"requestedCapabilities":["sessionMcp","sessionListStream"]}}}} +{"dir":"in","msg":{"id":1,"jsonrpc":"2.0","result":{"experimentalApi":true,"grantedCapabilities":["sessionMcp","sessionListStream"],"museHome":"/tmp/muse-phase0/data/muse","platformFamily":"unix","platformOs":"linux","schema":{"fingerprint":"sha256:e0e163db6ccf00dbe68402ce55d6319b3edc33c421f31e9583b587b2de8a118f","version":1},"serverInfo":{"name":"muse","version":"1.4.1"},"sessionDurability":"durable","userAgent":"muse-build/1.4.1 (non-interactive; linux-x86_64; build 35815c253477406321e3f5595195becc25e3907a)"}}} +{"dir":"out","msg":{"jsonrpc":"2.0","method":"initialized","params":{}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":2,"method":"session/start","params":{"commandId":"01a0ee48-a7c2-73c7-aab0-d5550547b265","workspaceRoot":"/tmp/muse-phase0/repo","modelId":"meta/muse-spark-1.3","providerId":"meta","approvalMode":"onRequest"}}} +{"dir":"in","msg":{"error":{"code":-32603,"data":{"kind":"internal"},"message":"internal error: compose session permission profile: permission profile ':auto-review' cannot be used: the automated reviewer is unavailable on this host"},"id":2,"jsonrpc":"2.0"}} diff --git a/plugins/muse-provider/test/fixtures/catalog-controls.ndjson b/plugins/muse-provider/test/fixtures/catalog-controls.ndjson new file mode 100644 index 0000000000..da27f307fd --- /dev/null +++ b/plugins/muse-provider/test/fixtures/catalog-controls.ndjson @@ -0,0 +1,26 @@ +{"dir":"out","msg":{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"clientInfo":{"name":"paseo_phase0","version":"0.0.0"},"capabilities":{"experimentalApi":true,"requestedCapabilities":["sessionMcp","sessionListStream"]}}}} +{"dir":"in","msg":{"id":1,"jsonrpc":"2.0","result":{"experimentalApi":true,"grantedCapabilities":["sessionMcp","sessionListStream"],"museHome":"/tmp/muse-phase0/data/muse","platformFamily":"unix","platformOs":"linux","schema":{"fingerprint":"sha256:e0e163db6ccf00dbe68402ce55d6319b3edc33c421f31e9583b587b2de8a118f","version":1},"serverInfo":{"name":"muse","version":"1.4.1"},"sessionDurability":"durable","userAgent":"muse-build/1.4.1 (non-interactive; linux-x86_64; build 35815c253477406321e3f5595195becc25e3907a)"}}} +{"dir":"out","msg":{"jsonrpc":"2.0","method":"initialized","params":{}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":2,"method":"session/start","params":{"commandId":"01a0ee4b-835a-7889-ae06-b9fa4195949d","workspaceRoot":"/tmp/muse-phase0/repo","modelId":"meta/muse-spark-1.3","providerId":"meta","approvalMode":"onRequest"}}} +{"dir":"in","msg":{"emittedAtMs":1790704321462,"jsonrpc":"2.0","method":"session/started","params":{"session":{"activeTurnId":null,"approvalMode":{"lastCommandId":null,"mode":"onRequest","source":"startup"},"createdAt":"2026-09-29T17:52:01.38362Z","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee4b-8364-7aa1-9ba3-c251d4cf0cb6/session.jsonl","providerId":"meta","sessionId":"01a0ee4b-8364-7aa1-9ba3-c251d4cf0cb6","status":"idle","turnCount":0,"updatedAt":"2026-09-29T17:52:01.383624Z","workspaceRoot":"/tmp/muse-phase0/repo"}}}} +{"dir":"in","msg":{"id":2,"jsonrpc":"2.0","result":{"session":{"activeTurnId":null,"approvalMode":{"lastCommandId":null,"mode":"onRequest","source":"startup"},"createdAt":"2026-09-29T17:52:01.38362Z","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee4b-8364-7aa1-9ba3-c251d4cf0cb6/session.jsonl","providerId":"meta","sessionId":"01a0ee4b-8364-7aa1-9ba3-c251d4cf0cb6","status":"idle","turnCount":0,"updatedAt":"2026-09-29T17:52:01.383624Z","workspaceRoot":"/tmp/muse-phase0/repo"},"viewCursor":"v:01a0ee4b-8364-7aa1-9ba3-c251d4cf0cb6:1"}}} +{"dir":"in","msg":{"emittedAtMs":1790704321467,"jsonrpc":"2.0","method":"session/approvalModeChanged","params":{"clientName":"paseo_phase0","commandId":"01a0ee4b-835a-7889-ae06-b9fa4195949d","mode":"onRequest","sessionId":"01a0ee4b-8364-7aa1-9ba3-c251d4cf0cb6","source":"approvalReconfigure","sourceRange":{"first":{"id":"61222bfc-8a69-487e-b930-ca6fa2befef2","sequence":5},"last":{"id":"61222bfc-8a69-487e-b930-ca6fa2befef2","sequence":5},"stream":{"id":"01a0ee4b-8364-7aa1-9ba3-c251d4cf0cb6","kind":"session"}},"viewCursor":"v:01a0ee4b-8364-7aa1-9ba3-c251d4cf0cb6:1"}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":3,"method":"model/list","params":{"sessionId":"01a0ee4b-8364-7aa1-9ba3-c251d4cf0cb6"}}} +{"dir":"in","msg":{"id":3,"jsonrpc":"2.0","result":{"models":[{"contextLimit":null,"cost":null,"defaultReasoningEffort":"high","description":null,"displayLabel":"meta/muse-spark-1.3","isActive":true,"isDefault":false,"modelId":"meta/muse-spark-1.3","outputLimit":null,"profileId":null,"providerId":"meta","reasoningEffortVariants":[],"releaseDate":null,"variants":["minimal","low","medium","high","xhigh"]}],"profileId":"tbh","providerId":"meta","source":"bundledCatalog"}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":4,"method":"account/read","params":{}}} +{"dir":"in","msg":{"id":4,"jsonrpc":"2.0","result":{"credentialRequired":true,"label":"META_API_KEY","state":"envKey"}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":5,"method":"usage/read","params":{}}} +{"dir":"in","msg":{"id":5,"jsonrpc":"2.0","result":{}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":6,"method":"session/list","params":{"workspaceRoot":"/tmp/muse-phase0/repo","limit":2}}} +{"dir":"in","msg":{"id":6,"jsonrpc":"2.0","result":{"nextCursor":"p:k:1790704314916673:01a0ee49-ff75-70e2-83fc-3169de40b80c:f509d55a","sessions":[{"activeTurnId":null,"createdAt":"2026-09-29T17:52:01.38362Z","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee4b-8364-7aa1-9ba3-c251d4cf0cb6/session.jsonl","providerId":"meta","sessionId":"01a0ee4b-8364-7aa1-9ba3-c251d4cf0cb6","status":"idle","turnCount":0,"updatedAt":"2026-09-29T17:52:01.460992Z","workspaceRoot":"/tmp/muse-phase0/repo"},{"activeTurnId":null,"branch":"main","createdAt":"2026-09-29T17:50:22.069281Z","firstUserPrompt":"Call the tool mcp__verification__verification_echo with empty arguments {}, then reply with the marker it returns. This is a tool invocation test, do not use skills, shell, or subagents.","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee49-ff75-70e2-83fc-3169de40b80c/session.jsonl","providerId":"meta","sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","status":"notLoaded","title":"Call the tool mcp__verification__verification_echo with empty arguments {}, then reply with the marker it returns. This is a tool invocation test, do not use skills, shell, or subagents.","turnCount":1,"updatedAt":"2026-09-29T17:51:54.916673Z","workspaceRoot":"/tmp/muse-phase0/repo"}]}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":7,"method":"session/setApprovalMode","params":{"commandId":"01a0ee4b-83bc-79d0-a6ac-ea983740fbf0","sessionId":"01a0ee4b-8364-7aa1-9ba3-c251d4cf0cb6","mode":"promptUnmatched"}}} +{"dir":"in","msg":{"emittedAtMs":1790704321487,"jsonrpc":"2.0","method":"session/approvalModeChanged","params":{"clientName":"paseo_phase0","commandId":"01a0ee4b-83bc-79d0-a6ac-ea983740fbf0","mode":"promptUnmatched","sessionId":"01a0ee4b-8364-7aa1-9ba3-c251d4cf0cb6","source":"approvalReconfigure","sourceRange":{"first":{"id":"60934282-4249-4369-a638-a18b304d1969","sequence":11},"last":{"id":"60934282-4249-4369-a638-a18b304d1969","sequence":11},"stream":{"id":"01a0ee4b-8364-7aa1-9ba3-c251d4cf0cb6","kind":"session"}},"viewCursor":"v:01a0ee4b-8364-7aa1-9ba3-c251d4cf0cb6:2"}}} +{"dir":"in","msg":{"id":7,"jsonrpc":"2.0","result":{"applyOutcome":"completed","commandId":"01a0ee4b-83bc-79d0-a6ac-ea983740fbf0","effectiveMode":{"lastCommandId":"01a0ee4b-83bc-79d0-a6ac-ea983740fbf0","mode":"promptUnmatched","source":"approvalReconfigure"},"status":"accepted"}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":8,"method":"session/setModel","params":{"commandId":"01a0ee4b-83bc-7705-a57e-c9666c4b4a38","sessionId":"01a0ee4b-8364-7aa1-9ba3-c251d4cf0cb6","model":{"providerId":"meta","modelId":"meta/muse-spark-1.3"}}}} +{"dir":"in","msg":{"emittedAtMs":1790704321491,"jsonrpc":"2.0","method":"session/listChanged","params":{"session":{"activeTurnId":null,"createdAt":"2026-09-29T17:52:01.38362Z","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee4b-8364-7aa1-9ba3-c251d4cf0cb6/session.jsonl","providerId":"meta","sessionId":"01a0ee4b-8364-7aa1-9ba3-c251d4cf0cb6","status":"idle","turnCount":0,"updatedAt":"2026-09-29T17:52:01.488381Z","workspaceRoot":"/tmp/muse-phase0/repo"}}}} +{"dir":"in","msg":{"id":8,"jsonrpc":"2.0","result":{"commandId":"01a0ee4b-83bc-7705-a57e-c9666c4b4a38","status":"accepted"}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":9,"method":"session/setModel","params":{"commandId":"01a0ee4b-83bc-7795-aaca-393dc487425c","sessionId":"01a0ee4b-8364-7aa1-9ba3-c251d4cf0cb6","model":{"providerId":"meta","modelId":"meta/muse-spark-1.2"}}}} +{"dir":"in","msg":{"error":{"code":-32030,"data":{"commandId":"01a0ee4b-83bc-7795-aaca-393dc487425c","kind":"commandRejected","reason":"invalid_model","retryable":false},"message":"session/setModel command 01a0ee4b-83bc-7795-aaca-393dc487425c rejected: invalid_model: model is not in the catalog"},"id":9,"jsonrpc":"2.0"}} +{"dir":"in","msg":{"emittedAtMs":1790704321500,"jsonrpc":"2.0","method":"session/closed","params":{"reason":"hostShutdown","sessionId":"01a0ee4b-8364-7aa1-9ba3-c251d4cf0cb6","viewCursor":"v:01a0ee4b-8364-7aa1-9ba3-c251d4cf0cb6:2"}}} +{"dir":"in","msg":{"emittedAtMs":1790704321500,"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a0ee4b-8364-7aa1-9ba3-c251d4cf0cb6","status":"notLoaded","viewCursor":"v:01a0ee4b-8364-7aa1-9ba3-c251d4cf0cb6:2"}}} +{"dir":"in","msg":{"emittedAtMs":1790704321500,"jsonrpc":"2.0","method":"session/listChanged","params":{"session":{"activeTurnId":null,"createdAt":"2026-09-29T17:52:01.38362Z","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee4b-8364-7aa1-9ba3-c251d4cf0cb6/session.jsonl","providerId":"meta","sessionId":"01a0ee4b-8364-7aa1-9ba3-c251d4cf0cb6","status":"notLoaded","turnCount":0,"updatedAt":"2026-09-29T17:52:01.491211Z","workspaceRoot":"/tmp/muse-phase0/repo"}}}} diff --git a/plugins/muse-provider/test/fixtures/image-followup.ndjson b/plugins/muse-provider/test/fixtures/image-followup.ndjson new file mode 100644 index 0000000000..653b9fbfed --- /dev/null +++ b/plugins/muse-provider/test/fixtures/image-followup.ndjson @@ -0,0 +1,51 @@ +{"dir":"out","msg":{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"clientInfo":{"name":"paseo_phase0","version":"0.0.0"},"capabilities":{"experimentalApi":true,"requestedCapabilities":["sessionMcp","sessionListStream"]}}}} +{"dir":"in","msg":{"id":1,"jsonrpc":"2.0","result":{"experimentalApi":true,"grantedCapabilities":["sessionMcp","sessionListStream"],"museHome":"/tmp/muse-phase0/data/muse","platformFamily":"unix","platformOs":"linux","schema":{"fingerprint":"sha256:e0e163db6ccf00dbe68402ce55d6319b3edc33c421f31e9583b587b2de8a118f","version":1},"serverInfo":{"name":"muse","version":"1.4.1"},"sessionDurability":"durable","userAgent":"muse-build/1.4.1 (non-interactive; linux-x86_64; build 35815c253477406321e3f5595195becc25e3907a)"}}} +{"dir":"out","msg":{"jsonrpc":"2.0","method":"initialized","params":{}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":2,"method":"session/start","params":{"commandId":"01a0ee48-14b3-790c-ab3a-39c72c612044","workspaceRoot":"/tmp/muse-phase0/repo","modelId":"meta/muse-spark-1.3","providerId":"meta","approvalMode":"onRequest"}}} +{"dir":"in","msg":{"emittedAtMs":1790704096526,"jsonrpc":"2.0","method":"session/started","params":{"session":{"activeTurnId":null,"approvalMode":{"lastCommandId":null,"mode":"onRequest","source":"startup"},"createdAt":"2026-09-29T17:48:16.44578Z","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee48-14bb-7c82-8d96-069dbc09cf11/session.jsonl","providerId":"meta","sessionId":"01a0ee48-14bb-7c82-8d96-069dbc09cf11","status":"idle","turnCount":0,"updatedAt":"2026-09-29T17:48:16.445784Z","workspaceRoot":"/tmp/muse-phase0/repo"}}}} +{"dir":"in","msg":{"id":2,"jsonrpc":"2.0","result":{"session":{"activeTurnId":null,"approvalMode":{"lastCommandId":null,"mode":"onRequest","source":"startup"},"createdAt":"2026-09-29T17:48:16.44578Z","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee48-14bb-7c82-8d96-069dbc09cf11/session.jsonl","providerId":"meta","sessionId":"01a0ee48-14bb-7c82-8d96-069dbc09cf11","status":"idle","turnCount":0,"updatedAt":"2026-09-29T17:48:16.445784Z","workspaceRoot":"/tmp/muse-phase0/repo"},"viewCursor":"v:01a0ee48-14bb-7c82-8d96-069dbc09cf11:1"}}} +{"dir":"in","msg":{"emittedAtMs":1790704096532,"jsonrpc":"2.0","method":"session/approvalModeChanged","params":{"clientName":"paseo_phase0","commandId":"01a0ee48-14b3-790c-ab3a-39c72c612044","mode":"onRequest","sessionId":"01a0ee48-14bb-7c82-8d96-069dbc09cf11","source":"approvalReconfigure","sourceRange":{"first":{"id":"f4ffd6ba-aa84-4544-837d-869e523de6c4","sequence":5},"last":{"id":"f4ffd6ba-aa84-4544-837d-869e523de6c4","sequence":5},"stream":{"id":"01a0ee48-14bb-7c82-8d96-069dbc09cf11","kind":"session"}},"viewCursor":"v:01a0ee48-14bb-7c82-8d96-069dbc09cf11:1"}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":3,"method":"turn/start","params":{"commandId":"01a0ee48-1515-7274-aba9-149303b45e94","sessionId":"01a0ee48-14bb-7c82-8d96-069dbc09cf11","input":[{"type":"text","text":"Describe this image in one short sentence. Do not use tools."},{"type":"image","mediaType":"image/png","base64Data":"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAF0lEQVR4nGP4z8BAEiJN9aiGUQ1DSgMAkPn/Afnh+ngAAAAASUVORK5CYII="}],"reasoningEffort":"low"}}} +{"dir":"in","msg":{"emittedAtMs":1790704096535,"jsonrpc":"2.0","method":"session/branchChanged","params":{"branch":"main","sessionId":"01a0ee48-14bb-7c82-8d96-069dbc09cf11","sourceRange":{"first":{"id":"e602d5e4-82d2-4572-8428-f508f63f9def","sequence":15},"last":{"id":"e602d5e4-82d2-4572-8428-f508f63f9def","sequence":15},"stream":{"id":"01a0ee48-14bb-7c82-8d96-069dbc09cf11","kind":"session"}},"vcs":"git","viewCursor":"v:01a0ee48-14bb-7c82-8d96-069dbc09cf11:2","workspaceRoot":"/tmp/muse-phase0/repo"}}} +{"dir":"in","msg":{"id":3,"jsonrpc":"2.0","result":{"commandId":"01a0ee48-1515-7274-aba9-149303b45e94","disposition":"started","startedNewTurn":true,"status":"accepted","turnId":"01a0ee48-1515-7274-aba9-149303b45e94"}}} +{"dir":"in","msg":{"emittedAtMs":1790704096544,"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a0ee48-14bb-7c82-8d96-069dbc09cf11","status":"running","viewCursor":"v:01a0ee48-14bb-7c82-8d96-069dbc09cf11:3"}}} +{"dir":"in","msg":{"emittedAtMs":1790704096545,"jsonrpc":"2.0","method":"turn/started","params":{"commandId":"01a0ee48-1515-7274-aba9-149303b45e94","sessionId":"01a0ee48-14bb-7c82-8d96-069dbc09cf11","sourceRange":{"first":{"id":"22ddcfee-c052-4512-b13a-16ddf7c359b5","sequence":17},"last":{"id":"22ddcfee-c052-4512-b13a-16ddf7c359b5","sequence":17},"stream":{"id":"01a0ee48-14bb-7c82-8d96-069dbc09cf11","kind":"session"}},"turnId":"01a0ee48-1515-7274-aba9-149303b45e94","viewCursor":"v:01a0ee48-14bb-7c82-8d96-069dbc09cf11:3"}}} +{"dir":"in","msg":{"emittedAtMs":1790704096545,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"attachments":[{"height":16,"mediaType":"image/png","type":"image","width":16}],"commandId":"01a0ee48-1515-7274-aba9-149303b45e94","itemId":"62642c88-dc30-4c0a-9063-845411f2b8ea","kind":"userMessage","recordedAt":"2026-09-29T17:48:16.536455Z","revision":1,"status":"completed","text":"Describe this image in one short sentence. Do not use tools.[Image #1]","turnId":"01a0ee48-1515-7274-aba9-149303b45e94"},"sessionId":"01a0ee48-14bb-7c82-8d96-069dbc09cf11","sourceRange":{"first":{"id":"22ddcfee-c052-4512-b13a-16ddf7c359b5","sequence":17},"last":{"id":"22ddcfee-c052-4512-b13a-16ddf7c359b5","sequence":17},"stream":{"id":"01a0ee48-14bb-7c82-8d96-069dbc09cf11","kind":"session"}},"viewCursor":"v:01a0ee48-14bb-7c82-8d96-069dbc09cf11:4"}}} +{"dir":"in","msg":{"emittedAtMs":1790704096545,"jsonrpc":"2.0","method":"item/updated","params":{"item":{"attachments":[{"height":16,"mediaType":"image/png","type":"image","width":16}],"commandId":"01a0ee48-1515-7274-aba9-149303b45e94","displayText":"Describe this image in one short sentence. Do not use tools.[Image #1]","itemId":"62642c88-dc30-4c0a-9063-845411f2b8ea","kind":"userMessage","recordedAt":"2026-09-29T17:48:16.537125Z","revision":2,"status":"completed","text":"Describe this image in one short sentence. Do not use tools.[Image #1]","turnId":"01a0ee48-1515-7274-aba9-149303b45e94"},"sessionId":"01a0ee48-14bb-7c82-8d96-069dbc09cf11","sourceRange":{"first":{"id":"24f5f31b-abad-4786-9a0c-3ca420a4fa9d","sequence":19},"last":{"id":"24f5f31b-abad-4786-9a0c-3ca420a4fa9d","sequence":19},"stream":{"id":"01a0ee48-14bb-7c82-8d96-069dbc09cf11","kind":"session"}},"viewCursor":"v:01a0ee48-14bb-7c82-8d96-069dbc09cf11:5"}}} +{"dir":"in","msg":{"emittedAtMs":1790704096545,"jsonrpc":"2.0","method":"item/started","params":{"item":{"childSessionId":"3691347e-482d-48ab-8914-7531c20e686c","fallbackText":"Reminder child session","generationId":1,"itemId":"86e3dc92-0522-443c-a8c2-a256279ed41f","kind":"reminderChild","recordedAt":"2026-09-29T17:48:16.544809Z","reminderAgentId":"skill-reminder","revision":1,"status":"inProgress","taskId":"01a0ee48-151f-74c0-ba8d-a9ce7cc3ba5a","turnId":"01a0ee48-1515-7274-aba9-149303b45e94"},"sessionId":"01a0ee48-14bb-7c82-8d96-069dbc09cf11","sourceRange":{"first":{"id":"86e3dc92-0522-443c-a8c2-a256279ed41f","sequence":36},"last":{"id":"86e3dc92-0522-443c-a8c2-a256279ed41f","sequence":36},"stream":{"id":"01a0ee48-14bb-7c82-8d96-069dbc09cf11","kind":"session"}},"viewCursor":"v:01a0ee48-14bb-7c82-8d96-069dbc09cf11:6"}}} +{"dir":"in","msg":{"emittedAtMs":1790704096550,"jsonrpc":"2.0","method":"session/listChanged","params":{"session":{"activeTurnId":"01a0ee48-1515-7274-aba9-149303b45e94","createdAt":"2026-09-29T17:48:16.44578Z","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee48-14bb-7c82-8d96-069dbc09cf11/session.jsonl","providerId":"meta","sessionId":"01a0ee48-14bb-7c82-8d96-069dbc09cf11","status":"running","turnCount":0,"updatedAt":"2026-09-29T17:48:16.545087Z","workspaceRoot":"/tmp/muse-phase0/repo"}}}} +{"dir":"in","msg":{"emittedAtMs":1790704102811,"jsonrpc":"2.0","method":"item/started","params":{"item":{"itemId":"192f882e-3314-4a38-ac9c-7ba2e2a7b1ab","kind":"reasoning","revision":1,"status":"inProgress","summary":[""],"turnId":"01a0ee48-1515-7274-aba9-149303b45e94"},"sessionId":"01a0ee48-14bb-7c82-8d96-069dbc09cf11","viewCursor":"v:01a0ee48-14bb-7c82-8d96-069dbc09cf11:6.1"}}} +{"dir":"in","msg":{"emittedAtMs":1790704102811,"jsonrpc":"2.0","method":"item/delta","params":{"delta":"Describing a low-resolution image in one short sentence under direct-answer constraints.","field":"summary.0","itemId":"192f882e-3314-4a38-ac9c-7ba2e2a7b1ab","sessionId":"01a0ee48-14bb-7c82-8d96-069dbc09cf11","viewCursor":"v:01a0ee48-14bb-7c82-8d96-069dbc09cf11:6.2"}}} +{"dir":"in","msg":{"emittedAtMs":1790704103089,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"itemId":"192f882e-3314-4a38-ac9c-7ba2e2a7b1ab","kind":"reasoning","providerItemId":"rs_6abbf9e2991c9b5a23f64316:rs_01a0ee481e9472efa2a19d22b36af83a","recordedAt":"2026-09-29T17:48:23.089292Z","revision":2,"status":"completed","summary":["Describing a low-resolution image in one short sentence under direct-answer constraints."],"turnId":"01a0ee48-1515-7274-aba9-149303b45e94"},"sessionId":"01a0ee48-14bb-7c82-8d96-069dbc09cf11","sourceRange":{"first":{"id":"e6ac2c82-8772-4f2c-97b6-8919ade00a40","sequence":49},"last":{"id":"e6ac2c82-8772-4f2c-97b6-8919ade00a40","sequence":49},"stream":{"id":"01a0ee48-14bb-7c82-8d96-069dbc09cf11","kind":"session"}},"viewCursor":"v:01a0ee48-14bb-7c82-8d96-069dbc09cf11:7"}}} +{"dir":"in","msg":{"emittedAtMs":1790704103090,"jsonrpc":"2.0","method":"item/started","params":{"item":{"itemId":"688360ec-f735-4d27-9461-495d7aac777d","kind":"agentMessage","revision":1,"status":"inProgress","text":"","turnId":"01a0ee48-1515-7274-aba9-149303b45e94"},"sessionId":"01a0ee48-14bb-7c82-8d96-069dbc09cf11","viewCursor":"v:01a0ee48-14bb-7c82-8d96-069dbc09cf11:7.1"}}} +{"dir":"in","msg":{"emittedAtMs":1790704103090,"jsonrpc":"2.0","method":"item/delta","params":{"delta":"The image is a tiny, blur","field":"text","itemId":"688360ec-f735-4d27-9461-495d7aac777d","sessionId":"01a0ee48-14bb-7c82-8d96-069dbc09cf11","viewCursor":"v:01a0ee48-14bb-7c82-8d96-069dbc09cf11:7.2"}}} +{"dir":"in","msg":{"emittedAtMs":1790704103090,"jsonrpc":"2.0","method":"item/delta","params":{"delta":"ry gray square.","field":"text","itemId":"688360ec-f735-4d27-9461-495d7aac777d","sessionId":"01a0ee48-14bb-7c82-8d96-069dbc09cf11","viewCursor":"v:01a0ee48-14bb-7c82-8d96-069dbc09cf11:7.3"}}} +{"dir":"in","msg":{"emittedAtMs":1790704103139,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"itemId":"688360ec-f735-4d27-9461-495d7aac777d","kind":"agentMessage","recordedAt":"2026-09-29T17:48:23.139316Z","revision":2,"status":"completed","text":"The image is a tiny, blurry gray square.","turnId":"01a0ee48-1515-7274-aba9-149303b45e94"},"sessionId":"01a0ee48-14bb-7c82-8d96-069dbc09cf11","sourceRange":{"first":{"id":"dce7d692-009d-46fd-94f8-91714848dd2a","sequence":51},"last":{"id":"dce7d692-009d-46fd-94f8-91714848dd2a","sequence":51},"stream":{"id":"01a0ee48-14bb-7c82-8d96-069dbc09cf11","kind":"session"}},"viewCursor":"v:01a0ee48-14bb-7c82-8d96-069dbc09cf11:8"}}} +{"dir":"in","msg":{"emittedAtMs":1790704103151,"jsonrpc":"2.0","method":"session/tokenUsage","params":{"cumulative":{"outputTokens":219,"promptTokens":38151,"totalTokens":38370},"durationMs":6602,"modelId":"meta/muse-spark-1.3","promptTokens":38151,"sessionId":"01a0ee48-14bb-7c82-8d96-069dbc09cf11","sourceRange":{"first":{"id":"fdcff71b-90b0-4acb-b6fe-c7f9b8c3fb6b","sequence":54},"last":{"id":"fdcff71b-90b0-4acb-b6fe-c7f9b8c3fb6b","sequence":54},"stream":{"id":"01a0ee48-14bb-7c82-8d96-069dbc09cf11","kind":"session"}},"totalTokens":38370,"turnId":"01a0ee48-1515-7274-aba9-149303b45e94","usage":{"cacheReadTokens":0,"cacheWriteTokens":0,"cachedTokens":0,"inputTokens":38151,"outputTokens":219,"reasoningTokens":179},"viewCursor":"v:01a0ee48-14bb-7c82-8d96-069dbc09cf11:9"}}} +{"dir":"in","msg":{"emittedAtMs":1790704103152,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"childSessionId":"3691347e-482d-48ab-8914-7531c20e686c","fallbackText":"Reminder child session","generationId":1,"itemId":"86e3dc92-0522-443c-a8c2-a256279ed41f","kind":"reminderChild","recordedAt":"2026-09-29T17:48:23.152017Z","reminderAgentId":"skill-reminder","revision":2,"status":"completed","taskId":"01a0ee48-151f-74c0-ba8d-a9ce7cc3ba5a","turnId":"01a0ee48-1515-7274-aba9-149303b45e94"},"sessionId":"01a0ee48-14bb-7c82-8d96-069dbc09cf11","sourceRange":{"first":{"id":"86e3dc92-0522-443c-a8c2-a256279ed41f","sequence":36},"last":{"id":"fa2b6f88-a391-484e-8fd9-3691601ac5e4","sequence":56},"stream":{"id":"01a0ee48-14bb-7c82-8d96-069dbc09cf11","kind":"session"}},"viewCursor":"v:01a0ee48-14bb-7c82-8d96-069dbc09cf11:10"}}} +{"dir":"in","msg":{"emittedAtMs":1790704103154,"jsonrpc":"2.0","method":"item/started","params":{"item":{"childSessionId":"16262f58-6c4c-4d2b-8097-48bbe0cb3596","childSessionLogPath":"subagent/16262f58-6c4c-4d2b-8097-48bbe0cb3596/session.jsonl","fallbackText":"Reminder child session","generationId":1,"itemId":"11032c1c-b9c0-4cb9-aa82-bfc56e21071e","kind":"reminderChild","recordedAt":"2026-09-29T17:48:23.154514Z","reminderAgentId":"verify-reminder","revision":1,"status":"inProgress","taskId":"01a0ee48-2ef1-7450-9ba1-f78bfe6dd324","turnId":"01a0ee48-1515-7274-aba9-149303b45e94"},"sessionId":"01a0ee48-14bb-7c82-8d96-069dbc09cf11","sourceRange":{"first":{"id":"11032c1c-b9c0-4cb9-aa82-bfc56e21071e","sequence":65},"last":{"id":"11032c1c-b9c0-4cb9-aa82-bfc56e21071e","sequence":65},"stream":{"id":"01a0ee48-14bb-7c82-8d96-069dbc09cf11","kind":"session"}},"viewCursor":"v:01a0ee48-14bb-7c82-8d96-069dbc09cf11:11"}}} +{"dir":"in","msg":{"emittedAtMs":1790704107897,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"childSessionId":"16262f58-6c4c-4d2b-8097-48bbe0cb3596","childSessionLogPath":"subagent/16262f58-6c4c-4d2b-8097-48bbe0cb3596/session.jsonl","fallbackText":"Reminder child session","generationId":1,"itemId":"11032c1c-b9c0-4cb9-aa82-bfc56e21071e","kind":"reminderChild","recordedAt":"2026-09-29T17:48:27.897303Z","reminderAgentId":"verify-reminder","revision":2,"status":"completed","taskId":"01a0ee48-2ef1-7450-9ba1-f78bfe6dd324","turnId":"01a0ee48-1515-7274-aba9-149303b45e94"},"sessionId":"01a0ee48-14bb-7c82-8d96-069dbc09cf11","sourceRange":{"first":{"id":"11032c1c-b9c0-4cb9-aa82-bfc56e21071e","sequence":65},"last":{"id":"e7c51564-f38c-4f0b-bc78-23d1c87dd0dc","sequence":67},"stream":{"id":"01a0ee48-14bb-7c82-8d96-069dbc09cf11","kind":"session"}},"viewCursor":"v:01a0ee48-14bb-7c82-8d96-069dbc09cf11:12"}}} +{"dir":"in","msg":{"emittedAtMs":1790704107898,"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a0ee48-14bb-7c82-8d96-069dbc09cf11","status":"idle","viewCursor":"v:01a0ee48-14bb-7c82-8d96-069dbc09cf11:13"}}} +{"dir":"in","msg":{"emittedAtMs":1790704107900,"jsonrpc":"2.0","method":"turn/completed","params":{"durationMs":11362,"sessionId":"01a0ee48-14bb-7c82-8d96-069dbc09cf11","sourceRange":{"first":{"id":"61602f64-1c16-4cd1-bf19-75b72ee45561","sequence":70},"last":{"id":"61602f64-1c16-4cd1-bf19-75b72ee45561","sequence":70},"stream":{"id":"01a0ee48-14bb-7c82-8d96-069dbc09cf11","kind":"session"}},"terminal":"completed","timeToFirstTokenMs":6275,"turnId":"01a0ee48-1515-7274-aba9-149303b45e94","viewCursor":"v:01a0ee48-14bb-7c82-8d96-069dbc09cf11:13"}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":4,"method":"turn/start","params":{"commandId":"01a0ee48-417c-7e7e-a5ce-124c68cfc434","sessionId":"01a0ee48-14bb-7c82-8d96-069dbc09cf11","input":[{"type":"text","text":"Reply with exactly AFTER_IMAGE_OK. Do not use tools."}],"reasoningEffort":"low"}}} +{"dir":"in","msg":{"emittedAtMs":1790704107900,"jsonrpc":"2.0","method":"session/branchChanged","params":{"branch":"main","sessionId":"01a0ee48-14bb-7c82-8d96-069dbc09cf11","sourceRange":{"first":{"id":"33c0c528-c953-4b80-a3a8-c12574a3485b","sequence":71},"last":{"id":"33c0c528-c953-4b80-a3a8-c12574a3485b","sequence":71},"stream":{"id":"01a0ee48-14bb-7c82-8d96-069dbc09cf11","kind":"session"}},"vcs":"git","viewCursor":"v:01a0ee48-14bb-7c82-8d96-069dbc09cf11:14","workspaceRoot":"/tmp/muse-phase0/repo"}}} +{"dir":"in","msg":{"emittedAtMs":1790704107900,"jsonrpc":"2.0","method":"session/listChanged","params":{"session":{"activeTurnId":null,"branch":"main","createdAt":"2026-09-29T17:48:16.443351Z","firstUserPrompt":"Describe this image in one short sentence. Do not use tools.[Image #1]","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee48-14bb-7c82-8d96-069dbc09cf11/session.jsonl","providerId":"meta","sessionId":"01a0ee48-14bb-7c82-8d96-069dbc09cf11","status":"idle","title":"Describe this image in one short sentence. Do not use tools.[Image #1]","turnCount":1,"updatedAt":"2026-09-29T17:48:27.898637Z","workspaceRoot":"/tmp/muse-phase0/repo"}}}} +{"dir":"in","msg":{"id":4,"jsonrpc":"2.0","result":{"commandId":"01a0ee48-417c-7e7e-a5ce-124c68cfc434","disposition":"started","startedNewTurn":true,"status":"accepted","turnId":"01a0ee48-417c-7e7e-a5ce-124c68cfc434"}}} +{"dir":"in","msg":{"emittedAtMs":1790704107927,"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a0ee48-14bb-7c82-8d96-069dbc09cf11","status":"running","viewCursor":"v:01a0ee48-14bb-7c82-8d96-069dbc09cf11:15"}}} +{"dir":"in","msg":{"emittedAtMs":1790704107928,"jsonrpc":"2.0","method":"turn/started","params":{"commandId":"01a0ee48-417c-7e7e-a5ce-124c68cfc434","sessionId":"01a0ee48-14bb-7c82-8d96-069dbc09cf11","sourceRange":{"first":{"id":"8700751a-c26e-40a5-bdeb-3f9ac3f3a3d0","sequence":77},"last":{"id":"8700751a-c26e-40a5-bdeb-3f9ac3f3a3d0","sequence":77},"stream":{"id":"01a0ee48-14bb-7c82-8d96-069dbc09cf11","kind":"session"}},"turnId":"01a0ee48-417c-7e7e-a5ce-124c68cfc434","viewCursor":"v:01a0ee48-14bb-7c82-8d96-069dbc09cf11:15"}}} +{"dir":"in","msg":{"emittedAtMs":1790704107928,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"commandId":"01a0ee48-417c-7e7e-a5ce-124c68cfc434","itemId":"845bb8ae-13c0-4319-98e0-2dd2d879261a","kind":"userMessage","recordedAt":"2026-09-29T17:48:27.905665Z","revision":1,"status":"completed","text":"Reply with exactly AFTER_IMAGE_OK. Do not use tools.","turnId":"01a0ee48-417c-7e7e-a5ce-124c68cfc434"},"sessionId":"01a0ee48-14bb-7c82-8d96-069dbc09cf11","sourceRange":{"first":{"id":"8700751a-c26e-40a5-bdeb-3f9ac3f3a3d0","sequence":77},"last":{"id":"8700751a-c26e-40a5-bdeb-3f9ac3f3a3d0","sequence":77},"stream":{"id":"01a0ee48-14bb-7c82-8d96-069dbc09cf11","kind":"session"}},"viewCursor":"v:01a0ee48-14bb-7c82-8d96-069dbc09cf11:16"}}} +{"dir":"in","msg":{"emittedAtMs":1790704107928,"jsonrpc":"2.0","method":"item/started","params":{"item":{"childSessionId":"8eb73549-16d3-411f-a0ba-22800a1c1b54","fallbackText":"Reminder child session","generationId":1,"itemId":"282c904c-d54f-467b-93e2-5b9480a166c5","kind":"reminderChild","recordedAt":"2026-09-29T17:48:27.917031Z","reminderAgentId":"skill-reminder","revision":1,"status":"inProgress","taskId":"01a0ee48-418b-7751-9429-a631c780d400","turnId":"01a0ee48-417c-7e7e-a5ce-124c68cfc434"},"sessionId":"01a0ee48-14bb-7c82-8d96-069dbc09cf11","sourceRange":{"first":{"id":"282c904c-d54f-467b-93e2-5b9480a166c5","sequence":95},"last":{"id":"282c904c-d54f-467b-93e2-5b9480a166c5","sequence":95},"stream":{"id":"01a0ee48-14bb-7c82-8d96-069dbc09cf11","kind":"session"}},"viewCursor":"v:01a0ee48-14bb-7c82-8d96-069dbc09cf11:17"}}} +{"dir":"in","msg":{"emittedAtMs":1790704107928,"jsonrpc":"2.0","method":"session/listChanged","params":{"session":{"activeTurnId":"01a0ee48-417c-7e7e-a5ce-124c68cfc434","branch":"main","createdAt":"2026-09-29T17:48:16.443351Z","firstUserPrompt":"Describe this image in one short sentence. Do not use tools.[Image #1]","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee48-14bb-7c82-8d96-069dbc09cf11/session.jsonl","providerId":"meta","sessionId":"01a0ee48-14bb-7c82-8d96-069dbc09cf11","status":"running","title":"Describe this image in one short sentence. Do not use tools.[Image #1]","turnCount":1,"updatedAt":"2026-09-29T17:48:27.920765Z","workspaceRoot":"/tmp/muse-phase0/repo"}}}} +{"dir":"in","msg":{"emittedAtMs":1790704111517,"jsonrpc":"2.0","method":"item/started","params":{"item":{"itemId":"32b4ec22-d9f6-473f-a024-3f83e998425f","kind":"agentMessage","revision":1,"status":"inProgress","text":"","turnId":"01a0ee48-417c-7e7e-a5ce-124c68cfc434"},"sessionId":"01a0ee48-14bb-7c82-8d96-069dbc09cf11","viewCursor":"v:01a0ee48-14bb-7c82-8d96-069dbc09cf11:17.1"}}} +{"dir":"in","msg":{"emittedAtMs":1790704111517,"jsonrpc":"2.0","method":"item/delta","params":{"delta":"AFTER_IMAGE_OK","field":"text","itemId":"32b4ec22-d9f6-473f-a024-3f83e998425f","sessionId":"01a0ee48-14bb-7c82-8d96-069dbc09cf11","viewCursor":"v:01a0ee48-14bb-7c82-8d96-069dbc09cf11:17.2"}}} +{"dir":"in","msg":{"emittedAtMs":1790704111606,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"itemId":"32b4ec22-d9f6-473f-a024-3f83e998425f","kind":"agentMessage","recordedAt":"2026-09-29T17:48:31.605848Z","revision":2,"status":"completed","text":"AFTER_IMAGE_OK","turnId":"01a0ee48-417c-7e7e-a5ce-124c68cfc434"},"sessionId":"01a0ee48-14bb-7c82-8d96-069dbc09cf11","sourceRange":{"first":{"id":"bbc74b47-606f-4003-8633-6112d1530645","sequence":108},"last":{"id":"bbc74b47-606f-4003-8633-6112d1530645","sequence":108},"stream":{"id":"01a0ee48-14bb-7c82-8d96-069dbc09cf11","kind":"session"}},"viewCursor":"v:01a0ee48-14bb-7c82-8d96-069dbc09cf11:18"}}} +{"dir":"in","msg":{"emittedAtMs":1790704111610,"jsonrpc":"2.0","method":"session/tokenUsage","params":{"cumulative":{"outputTokens":253,"promptTokens":76520,"totalTokens":76773},"durationMs":3689,"modelId":"meta/muse-spark-1.3","promptTokens":38369,"sessionId":"01a0ee48-14bb-7c82-8d96-069dbc09cf11","sourceRange":{"first":{"id":"61c466ae-5b9e-46ac-b77b-7d79ccdb95ee","sequence":111},"last":{"id":"61c466ae-5b9e-46ac-b77b-7d79ccdb95ee","sequence":111},"stream":{"id":"01a0ee48-14bb-7c82-8d96-069dbc09cf11","kind":"session"}},"totalTokens":38403,"turnId":"01a0ee48-417c-7e7e-a5ce-124c68cfc434","usage":{"cacheReadTokens":0,"cacheWriteTokens":0,"cachedTokens":0,"inputTokens":38369,"outputTokens":34,"reasoningTokens":20},"viewCursor":"v:01a0ee48-14bb-7c82-8d96-069dbc09cf11:19"}}} +{"dir":"in","msg":{"emittedAtMs":1790704111612,"jsonrpc":"2.0","method":"item/started","params":{"item":{"childSessionId":"a5b9e5ef-4db4-4de9-b43b-1709104635f9","childSessionLogPath":"subagent/a5b9e5ef-4db4-4de9-b43b-1709104635f9/session.jsonl","fallbackText":"Reminder child session","generationId":1,"itemId":"cbe47e00-7517-4e1b-82d5-ed95bf1e7c4f","kind":"reminderChild","recordedAt":"2026-09-29T17:48:31.612212Z","reminderAgentId":"verify-reminder","revision":1,"status":"inProgress","taskId":"01a0ee48-4ffa-7d20-8d06-65214075eec3","turnId":"01a0ee48-417c-7e7e-a5ce-124c68cfc434"},"sessionId":"01a0ee48-14bb-7c82-8d96-069dbc09cf11","sourceRange":{"first":{"id":"cbe47e00-7517-4e1b-82d5-ed95bf1e7c4f","sequence":118},"last":{"id":"cbe47e00-7517-4e1b-82d5-ed95bf1e7c4f","sequence":118},"stream":{"id":"01a0ee48-14bb-7c82-8d96-069dbc09cf11","kind":"session"}},"viewCursor":"v:01a0ee48-14bb-7c82-8d96-069dbc09cf11:20"}}} +{"dir":"in","msg":{"emittedAtMs":1790704116018,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"childSessionId":"8eb73549-16d3-411f-a0ba-22800a1c1b54","fallbackText":"Reminder child session","generationId":1,"itemId":"282c904c-d54f-467b-93e2-5b9480a166c5","kind":"reminderChild","recordedAt":"2026-09-29T17:48:36.017946Z","reminderAgentId":"skill-reminder","revision":2,"status":"completed","taskId":"01a0ee48-418b-7751-9429-a631c780d400","turnId":"01a0ee48-417c-7e7e-a5ce-124c68cfc434"},"sessionId":"01a0ee48-14bb-7c82-8d96-069dbc09cf11","sourceRange":{"first":{"id":"282c904c-d54f-467b-93e2-5b9480a166c5","sequence":95},"last":{"id":"5f3e4559-d00b-4404-88cc-bfb702ba426d","sequence":120},"stream":{"id":"01a0ee48-14bb-7c82-8d96-069dbc09cf11","kind":"session"}},"viewCursor":"v:01a0ee48-14bb-7c82-8d96-069dbc09cf11:21"}}} +{"dir":"in","msg":{"emittedAtMs":1790704116018,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"childSessionId":"a5b9e5ef-4db4-4de9-b43b-1709104635f9","childSessionLogPath":"subagent/a5b9e5ef-4db4-4de9-b43b-1709104635f9/session.jsonl","fallbackText":"Reminder child session","generationId":1,"itemId":"cbe47e00-7517-4e1b-82d5-ed95bf1e7c4f","kind":"reminderChild","recordedAt":"2026-09-29T17:48:36.018337Z","reminderAgentId":"verify-reminder","revision":2,"status":"completed","taskId":"01a0ee48-4ffa-7d20-8d06-65214075eec3","turnId":"01a0ee48-417c-7e7e-a5ce-124c68cfc434"},"sessionId":"01a0ee48-14bb-7c82-8d96-069dbc09cf11","sourceRange":{"first":{"id":"cbe47e00-7517-4e1b-82d5-ed95bf1e7c4f","sequence":118},"last":{"id":"60cf2710-1caa-4385-9b24-38d9cffb0b52","sequence":121},"stream":{"id":"01a0ee48-14bb-7c82-8d96-069dbc09cf11","kind":"session"}},"viewCursor":"v:01a0ee48-14bb-7c82-8d96-069dbc09cf11:22"}}} +{"dir":"in","msg":{"emittedAtMs":1790704116020,"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a0ee48-14bb-7c82-8d96-069dbc09cf11","status":"idle","viewCursor":"v:01a0ee48-14bb-7c82-8d96-069dbc09cf11:23"}}} +{"dir":"in","msg":{"emittedAtMs":1790704116021,"jsonrpc":"2.0","method":"turn/completed","params":{"durationMs":8114,"sessionId":"01a0ee48-14bb-7c82-8d96-069dbc09cf11","sourceRange":{"first":{"id":"98be3596-abf4-4650-9eea-20f3c3788dff","sequence":127},"last":{"id":"98be3596-abf4-4650-9eea-20f3c3788dff","sequence":127},"stream":{"id":"01a0ee48-14bb-7c82-8d96-069dbc09cf11","kind":"session"}},"terminal":"completed","timeToFirstTokenMs":3611,"turnId":"01a0ee48-417c-7e7e-a5ce-124c68cfc434","viewCursor":"v:01a0ee48-14bb-7c82-8d96-069dbc09cf11:23"}}} +{"dir":"in","msg":{"emittedAtMs":1790704116021,"jsonrpc":"2.0","method":"session/listChanged","params":{"session":{"activeTurnId":null,"branch":"main","createdAt":"2026-09-29T17:48:16.443351Z","firstUserPrompt":"Describe this image in one short sentence. Do not use tools.[Image #1]","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee48-14bb-7c82-8d96-069dbc09cf11/session.jsonl","providerId":"meta","sessionId":"01a0ee48-14bb-7c82-8d96-069dbc09cf11","status":"idle","title":"Describe this image in one short sentence. Do not use tools.[Image #1]","turnCount":2,"updatedAt":"2026-09-29T17:48:36.019856Z","workspaceRoot":"/tmp/muse-phase0/repo"}}}} +{"dir":"in","msg":{"emittedAtMs":1790704116224,"jsonrpc":"2.0","method":"session/closed","params":{"reason":"hostShutdown","sessionId":"01a0ee48-14bb-7c82-8d96-069dbc09cf11","viewCursor":"v:01a0ee48-14bb-7c82-8d96-069dbc09cf11:23"}}} +{"dir":"in","msg":{"emittedAtMs":1790704116224,"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a0ee48-14bb-7c82-8d96-069dbc09cf11","status":"notLoaded","viewCursor":"v:01a0ee48-14bb-7c82-8d96-069dbc09cf11:23"}}} +{"dir":"in","msg":{"emittedAtMs":1790704116224,"jsonrpc":"2.0","method":"session/listChanged","params":{"session":{"activeTurnId":null,"branch":"main","createdAt":"2026-09-29T17:48:16.443351Z","firstUserPrompt":"Describe this image in one short sentence. Do not use tools.[Image #1]","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee48-14bb-7c82-8d96-069dbc09cf11/session.jsonl","providerId":"meta","sessionId":"01a0ee48-14bb-7c82-8d96-069dbc09cf11","status":"notLoaded","title":"Describe this image in one short sentence. Do not use tools.[Image #1]","turnCount":2,"updatedAt":"2026-09-29T17:48:36.019856Z","workspaceRoot":"/tmp/muse-phase0/repo"}}}} diff --git a/plugins/muse-provider/test/fixtures/initialize-history.ndjson b/plugins/muse-provider/test/fixtures/initialize-history.ndjson new file mode 100644 index 0000000000..9fe32e2cc6 --- /dev/null +++ b/plugins/muse-provider/test/fixtures/initialize-history.ndjson @@ -0,0 +1,3 @@ +{"dir":"out","msg":{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"clientInfo":{"name":"paseo_phase0","version":"0.0.0"},"capabilities":{"experimentalApi":true,"requestedCapabilities":["sessionMcp","sessionListStream"]}}}} +{"dir":"in","msg":{"id":1,"jsonrpc":"2.0","result":{"experimentalApi":true,"grantedCapabilities":["sessionMcp","sessionListStream"],"museHome":"/tmp/muse-phase0/data/muse","platformFamily":"unix","platformOs":"linux","schema":{"fingerprint":"sha256:e0e163db6ccf00dbe68402ce55d6319b3edc33c421f31e9583b587b2de8a118f","version":1},"serverInfo":{"name":"muse","version":"1.4.1"},"sessionDurability":"durable","userAgent":"muse-build/1.4.1 (non-interactive; linux-x86_64; build 35815c253477406321e3f5595195becc25e3907a)"}}} +{"dir":"out","msg":{"jsonrpc":"2.0","method":"initialized","params":{}}} diff --git a/plugins/muse-provider/test/fixtures/interrupt.ndjson b/plugins/muse-provider/test/fixtures/interrupt.ndjson new file mode 100644 index 0000000000..591f0148ab --- /dev/null +++ b/plugins/muse-provider/test/fixtures/interrupt.ndjson @@ -0,0 +1,28 @@ +{"dir":"out","msg":{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"clientInfo":{"name":"paseo_phase0","version":"0.0.0"},"capabilities":{"experimentalApi":true,"requestedCapabilities":["sessionMcp","sessionListStream"]}}}} +{"dir":"in","msg":{"id":1,"jsonrpc":"2.0","result":{"experimentalApi":true,"grantedCapabilities":["sessionMcp","sessionListStream"],"museHome":"/tmp/muse-phase0/data/muse","platformFamily":"unix","platformOs":"linux","schema":{"fingerprint":"sha256:e0e163db6ccf00dbe68402ce55d6319b3edc33c421f31e9583b587b2de8a118f","version":1},"serverInfo":{"name":"muse","version":"1.4.1"},"sessionDurability":"durable","userAgent":"muse-build/1.4.1 (non-interactive; linux-x86_64; build 35815c253477406321e3f5595195becc25e3907a)"}}} +{"dir":"out","msg":{"jsonrpc":"2.0","method":"initialized","params":{}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":2,"method":"session/start","params":{"commandId":"01a0ee46-2c0f-7cba-a683-2424f20f00b7","workspaceRoot":"/tmp/muse-phase0/repo","modelId":"meta/muse-spark-1.3","providerId":"meta","approvalMode":"onRequest"}}} +{"dir":"in","msg":{"emittedAtMs":1790703971433,"jsonrpc":"2.0","method":"session/started","params":{"session":{"activeTurnId":null,"approvalMode":{"lastCommandId":null,"mode":"onRequest","source":"startup"},"createdAt":"2026-09-29T17:46:11.353718Z","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee46-2c17-73c1-935d-9963d05b7f69/session.jsonl","providerId":"meta","sessionId":"01a0ee46-2c17-73c1-935d-9963d05b7f69","status":"idle","turnCount":0,"updatedAt":"2026-09-29T17:46:11.353722Z","workspaceRoot":"/tmp/muse-phase0/repo"}}}} +{"dir":"in","msg":{"id":2,"jsonrpc":"2.0","result":{"session":{"activeTurnId":null,"approvalMode":{"lastCommandId":null,"mode":"onRequest","source":"startup"},"createdAt":"2026-09-29T17:46:11.353718Z","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee46-2c17-73c1-935d-9963d05b7f69/session.jsonl","providerId":"meta","sessionId":"01a0ee46-2c17-73c1-935d-9963d05b7f69","status":"idle","turnCount":0,"updatedAt":"2026-09-29T17:46:11.353722Z","workspaceRoot":"/tmp/muse-phase0/repo"},"viewCursor":"v:01a0ee46-2c17-73c1-935d-9963d05b7f69:1"}}} +{"dir":"in","msg":{"emittedAtMs":1790703971439,"jsonrpc":"2.0","method":"session/approvalModeChanged","params":{"clientName":"paseo_phase0","commandId":"01a0ee46-2c0f-7cba-a683-2424f20f00b7","mode":"onRequest","sessionId":"01a0ee46-2c17-73c1-935d-9963d05b7f69","source":"approvalReconfigure","sourceRange":{"first":{"id":"c5a5a50e-b360-485b-a2d9-1083a885b24a","sequence":5},"last":{"id":"c5a5a50e-b360-485b-a2d9-1083a885b24a","sequence":5},"stream":{"id":"01a0ee46-2c17-73c1-935d-9963d05b7f69","kind":"session"}},"viewCursor":"v:01a0ee46-2c17-73c1-935d-9963d05b7f69:1"}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":3,"method":"turn/start","params":{"commandId":"01a0ee46-2c70-7ee7-a0d9-c5b4a4efd05c","sessionId":"01a0ee46-2c17-73c1-935d-9963d05b7f69","input":[{"type":"text","text":"Run bash command sleep 12; then respond ORIGINAL_DONE. Do not respond before the command finishes."}],"reasoningEffort":"low"}}} +{"dir":"in","msg":{"emittedAtMs":1790703971442,"jsonrpc":"2.0","method":"session/branchChanged","params":{"branch":"main","sessionId":"01a0ee46-2c17-73c1-935d-9963d05b7f69","sourceRange":{"first":{"id":"39a5a9a9-3fad-4ad0-9eed-6bd5f4cc5f12","sequence":15},"last":{"id":"39a5a9a9-3fad-4ad0-9eed-6bd5f4cc5f12","sequence":15},"stream":{"id":"01a0ee46-2c17-73c1-935d-9963d05b7f69","kind":"session"}},"vcs":"git","viewCursor":"v:01a0ee46-2c17-73c1-935d-9963d05b7f69:2","workspaceRoot":"/tmp/muse-phase0/repo"}}} +{"dir":"in","msg":{"id":3,"jsonrpc":"2.0","result":{"commandId":"01a0ee46-2c70-7ee7-a0d9-c5b4a4efd05c","disposition":"started","startedNewTurn":true,"status":"accepted","turnId":"01a0ee46-2c70-7ee7-a0d9-c5b4a4efd05c"}}} +{"dir":"in","msg":{"emittedAtMs":1790703971449,"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a0ee46-2c17-73c1-935d-9963d05b7f69","status":"running","viewCursor":"v:01a0ee46-2c17-73c1-935d-9963d05b7f69:3"}}} +{"dir":"in","msg":{"emittedAtMs":1790703971450,"jsonrpc":"2.0","method":"turn/started","params":{"commandId":"01a0ee46-2c70-7ee7-a0d9-c5b4a4efd05c","sessionId":"01a0ee46-2c17-73c1-935d-9963d05b7f69","sourceRange":{"first":{"id":"32c5743a-d7bc-42e6-8675-7ed4a2e2796d","sequence":17},"last":{"id":"32c5743a-d7bc-42e6-8675-7ed4a2e2796d","sequence":17},"stream":{"id":"01a0ee46-2c17-73c1-935d-9963d05b7f69","kind":"session"}},"turnId":"01a0ee46-2c70-7ee7-a0d9-c5b4a4efd05c","viewCursor":"v:01a0ee46-2c17-73c1-935d-9963d05b7f69:3"}}} +{"dir":"in","msg":{"emittedAtMs":1790703971450,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"commandId":"01a0ee46-2c70-7ee7-a0d9-c5b4a4efd05c","itemId":"ba26432c-5367-4d56-a1af-da45202f42a2","kind":"userMessage","recordedAt":"2026-09-29T17:46:11.442978Z","revision":1,"status":"completed","text":"Run bash command sleep 12; then respond ORIGINAL_DONE. Do not respond before the command finishes.","turnId":"01a0ee46-2c70-7ee7-a0d9-c5b4a4efd05c"},"sessionId":"01a0ee46-2c17-73c1-935d-9963d05b7f69","sourceRange":{"first":{"id":"32c5743a-d7bc-42e6-8675-7ed4a2e2796d","sequence":17},"last":{"id":"32c5743a-d7bc-42e6-8675-7ed4a2e2796d","sequence":17},"stream":{"id":"01a0ee46-2c17-73c1-935d-9963d05b7f69","kind":"session"}},"viewCursor":"v:01a0ee46-2c17-73c1-935d-9963d05b7f69:4"}}} +{"dir":"in","msg":{"emittedAtMs":1790703971450,"jsonrpc":"2.0","method":"item/started","params":{"item":{"childSessionId":"18b1f3a2-c46a-4a36-88a3-f5a8eb1e78b1","fallbackText":"Reminder child session","generationId":1,"itemId":"24231a9c-6230-4ef4-b7ce-39710d296461","kind":"reminderChild","recordedAt":"2026-09-29T17:46:11.450333Z","reminderAgentId":"skill-reminder","revision":1,"status":"inProgress","taskId":"01a0ee46-2c78-7610-8396-67c912009ca1","turnId":"01a0ee46-2c70-7ee7-a0d9-c5b4a4efd05c"},"sessionId":"01a0ee46-2c17-73c1-935d-9963d05b7f69","sourceRange":{"first":{"id":"24231a9c-6230-4ef4-b7ce-39710d296461","sequence":35},"last":{"id":"24231a9c-6230-4ef4-b7ce-39710d296461","sequence":35},"stream":{"id":"01a0ee46-2c17-73c1-935d-9963d05b7f69","kind":"session"}},"viewCursor":"v:01a0ee46-2c17-73c1-935d-9963d05b7f69:5"}}} +{"dir":"in","msg":{"emittedAtMs":1790703971455,"jsonrpc":"2.0","method":"session/listChanged","params":{"session":{"activeTurnId":"01a0ee46-2c70-7ee7-a0d9-c5b4a4efd05c","createdAt":"2026-09-29T17:46:11.353718Z","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee46-2c17-73c1-935d-9963d05b7f69/session.jsonl","providerId":"meta","sessionId":"01a0ee46-2c17-73c1-935d-9963d05b7f69","status":"running","turnCount":0,"updatedAt":"2026-09-29T17:46:11.450604Z","workspaceRoot":"/tmp/muse-phase0/repo"}}}} +{"dir":"in","msg":{"emittedAtMs":1790703974977,"jsonrpc":"2.0","method":"session/tokenUsage","params":{"cumulative":{"outputTokens":165,"promptTokens":38143,"totalTokens":38308},"durationMs":3523,"finishReason":"tool_calls","modelId":"meta/muse-spark-1.3","promptTokens":38143,"sessionId":"01a0ee46-2c17-73c1-935d-9963d05b7f69","sourceRange":{"first":{"id":"0511cfd0-3bb6-400c-85e3-8e01f9dd5e29","sequence":51},"last":{"id":"0511cfd0-3bb6-400c-85e3-8e01f9dd5e29","sequence":51},"stream":{"id":"01a0ee46-2c17-73c1-935d-9963d05b7f69","kind":"session"}},"totalTokens":38308,"turnId":"01a0ee46-2c70-7ee7-a0d9-c5b4a4efd05c","usage":{"cacheReadTokens":0,"cacheWriteTokens":0,"cachedTokens":0,"inputTokens":38143,"outputTokens":165,"reasoningTokens":54},"viewCursor":"v:01a0ee46-2c17-73c1-935d-9963d05b7f69:6"}}} +{"dir":"in","msg":{"emittedAtMs":1790703974980,"jsonrpc":"2.0","method":"item/started","params":{"item":{"args":"{\"command\":\"sleep 12\",\"description\":\"Sleep for twelve seconds\",\"yield_time_ms\":15000}","callId":"call_01a0ee46375976599e7796d9cb6263a3","itemId":"01a0ee46-3a42-76f2-8fe8-f64de143cb33","kind":"toolCall","recordedAt":"2026-09-29T17:46:14.980372Z","revision":1,"status":"inProgress","tool":"bash","turnId":"01a0ee46-2c70-7ee7-a0d9-c5b4a4efd05c"},"sessionId":"01a0ee46-2c17-73c1-935d-9963d05b7f69","sourceRange":{"first":{"id":"4eabdc2b-0f6c-47ef-87d3-8d42ba628567","sequence":53},"last":{"id":"e240d66b-2bc9-417d-9c0f-e7547a261e4f","sequence":59},"stream":{"id":"01a0ee46-2c17-73c1-935d-9963d05b7f69","kind":"session"}},"viewCursor":"v:01a0ee46-2c17-73c1-935d-9963d05b7f69:7"}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":4,"method":"turn/interrupt","params":{"commandId":"01a0ee46-3e2c-792b-abd7-c755e457c361","sessionId":"01a0ee46-2c17-73c1-935d-9963d05b7f69","turnId":"01a0ee46-2c70-7ee7-a0d9-c5b4a4efd05c"}}} +{"dir":"in","msg":{"id":4,"jsonrpc":"2.0","result":{"commandId":"01a0ee46-3e2c-792b-abd7-c755e457c361","status":"accepted","turnId":"01a0ee46-2c70-7ee7-a0d9-c5b4a4efd05c"}}} +{"dir":"in","msg":{"emittedAtMs":1790703976031,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"args":"{\"command\":\"sleep 12\",\"description\":\"Sleep for twelve seconds\",\"yield_time_ms\":15000}","callId":"call_01a0ee46375976599e7796d9cb6263a3","failureReason":"cancelled by tool cancellation","itemId":"01a0ee46-3a42-76f2-8fe8-f64de143cb33","kind":"toolCall","recordedAt":"2026-09-29T17:46:16.031124Z","revision":2,"status":"cancelled","tool":"bash","turnId":"01a0ee46-2c70-7ee7-a0d9-c5b4a4efd05c","visibleOutput":"{\n \"chunk_id\": \"exec-1-1\",\n \"command\": \"sleep 12\",\n \"description\": \"Sleep for twelve seconds\",\n \"terminal_status\": \"cancelled\",\n \"terminal_reason\": \"cancelled by tool cancellation\",\n \"output\": \"\",\n \"original_output_bytes\": 0,\n \"original_output_tokens\": 0,\n \"truncated\": false\n}"},"sessionId":"01a0ee46-2c17-73c1-935d-9963d05b7f69","sourceRange":{"first":{"id":"4eabdc2b-0f6c-47ef-87d3-8d42ba628567","sequence":53},"last":{"id":"851e1513-f831-4d52-a1ef-f7caf0016653","sequence":64},"stream":{"id":"01a0ee46-2c17-73c1-935d-9963d05b7f69","kind":"session"}},"viewCursor":"v:01a0ee46-2c17-73c1-935d-9963d05b7f69:8"}}} +{"dir":"in","msg":{"emittedAtMs":1790703976032,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"childSessionId":"18b1f3a2-c46a-4a36-88a3-f5a8eb1e78b1","failureReason":"main run cancelled","fallbackText":"Reminder child session","generationId":1,"itemId":"24231a9c-6230-4ef4-b7ce-39710d296461","kind":"reminderChild","recordedAt":"2026-09-29T17:46:16.032366Z","reminderAgentId":"skill-reminder","revision":2,"status":"cancelled","taskId":"01a0ee46-2c78-7610-8396-67c912009ca1","turnId":"01a0ee46-2c70-7ee7-a0d9-c5b4a4efd05c"},"sessionId":"01a0ee46-2c17-73c1-935d-9963d05b7f69","sourceRange":{"first":{"id":"24231a9c-6230-4ef4-b7ce-39710d296461","sequence":35},"last":{"id":"fcd59a42-cd98-4dc3-b572-0ad06cb83bd3","sequence":68},"stream":{"id":"01a0ee46-2c17-73c1-935d-9963d05b7f69","kind":"session"}},"viewCursor":"v:01a0ee46-2c17-73c1-935d-9963d05b7f69:9"}}} +{"dir":"in","msg":{"emittedAtMs":1790703976032,"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a0ee46-2c17-73c1-935d-9963d05b7f69","status":"idle","viewCursor":"v:01a0ee46-2c17-73c1-935d-9963d05b7f69:10"}}} +{"dir":"in","msg":{"emittedAtMs":1790703976033,"jsonrpc":"2.0","method":"turn/completed","params":{"durationMs":4589,"reason":"cancelled after tool result reconciliation","sessionId":"01a0ee46-2c17-73c1-935d-9963d05b7f69","sourceRange":{"first":{"id":"13edc96a-f33d-4343-b40b-e4632258bdb0","sequence":70},"last":{"id":"13edc96a-f33d-4343-b40b-e4632258bdb0","sequence":70},"stream":{"id":"01a0ee46-2c17-73c1-935d-9963d05b7f69","kind":"session"}},"terminal":"cancelled","turnId":"01a0ee46-2c70-7ee7-a0d9-c5b4a4efd05c","viewCursor":"v:01a0ee46-2c17-73c1-935d-9963d05b7f69:10"}}} +{"dir":"in","msg":{"emittedAtMs":1790703976033,"jsonrpc":"2.0","method":"session/branchChanged","params":{"branch":"main","sessionId":"01a0ee46-2c17-73c1-935d-9963d05b7f69","sourceRange":{"first":{"id":"91e843be-9782-495d-8ca9-3032d4715a70","sequence":71},"last":{"id":"91e843be-9782-495d-8ca9-3032d4715a70","sequence":71},"stream":{"id":"01a0ee46-2c17-73c1-935d-9963d05b7f69","kind":"session"}},"vcs":"git","viewCursor":"v:01a0ee46-2c17-73c1-935d-9963d05b7f69:11","workspaceRoot":"/tmp/muse-phase0/repo"}}} +{"dir":"in","msg":{"emittedAtMs":1790703976033,"jsonrpc":"2.0","method":"session/listChanged","params":{"session":{"activeTurnId":null,"branch":"main","createdAt":"2026-09-29T17:46:11.351253Z","firstUserPrompt":"Run bash command sleep 12; then respond ORIGINAL_DONE. Do not respond before the command finishes.","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee46-2c17-73c1-935d-9963d05b7f69/session.jsonl","providerId":"meta","sessionId":"01a0ee46-2c17-73c1-935d-9963d05b7f69","status":"idle","title":"Run bash command sleep 12; then respond ORIGINAL_DONE. Do not respond before the command finishes.","turnCount":1,"updatedAt":"2026-09-29T17:46:16.032578Z","workspaceRoot":"/tmp/muse-phase0/repo"}}}} +{"dir":"in","msg":{"emittedAtMs":1790703976246,"jsonrpc":"2.0","method":"session/closed","params":{"reason":"hostShutdown","sessionId":"01a0ee46-2c17-73c1-935d-9963d05b7f69","viewCursor":"v:01a0ee46-2c17-73c1-935d-9963d05b7f69:11"}}} +{"dir":"in","msg":{"emittedAtMs":1790703976246,"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a0ee46-2c17-73c1-935d-9963d05b7f69","status":"notLoaded","viewCursor":"v:01a0ee46-2c17-73c1-935d-9963d05b7f69:11"}}} +{"dir":"in","msg":{"emittedAtMs":1790703976246,"jsonrpc":"2.0","method":"session/listChanged","params":{"session":{"activeTurnId":null,"branch":"main","createdAt":"2026-09-29T17:46:11.351253Z","firstUserPrompt":"Run bash command sleep 12; then respond ORIGINAL_DONE. Do not respond before the command finishes.","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee46-2c17-73c1-935d-9963d05b7f69/session.jsonl","providerId":"meta","sessionId":"01a0ee46-2c17-73c1-935d-9963d05b7f69","status":"notLoaded","title":"Run bash command sleep 12; then respond ORIGINAL_DONE. Do not respond before the command finishes.","turnCount":1,"updatedAt":"2026-09-29T17:46:16.033681Z","workspaceRoot":"/tmp/muse-phase0/repo"}}}} diff --git a/plugins/muse-provider/test/fixtures/mcp-stdio.ndjson b/plugins/muse-provider/test/fixtures/mcp-stdio.ndjson new file mode 100644 index 0000000000..e23f22335e --- /dev/null +++ b/plugins/muse-provider/test/fixtures/mcp-stdio.ndjson @@ -0,0 +1,97 @@ +{"dir":"out","msg":{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"clientInfo":{"name":"paseo_phase0","version":"0.0.0"},"capabilities":{"experimentalApi":true,"requestedCapabilities":["sessionMcp","sessionListStream"]}}}} +{"dir":"in","msg":{"id":1,"jsonrpc":"2.0","result":{"experimentalApi":true,"grantedCapabilities":["sessionMcp","sessionListStream"],"museHome":"/tmp/muse-phase0/data/muse","platformFamily":"unix","platformOs":"linux","schema":{"fingerprint":"sha256:e0e163db6ccf00dbe68402ce55d6319b3edc33c421f31e9583b587b2de8a118f","version":1},"serverInfo":{"name":"muse","version":"1.4.1"},"sessionDurability":"durable","userAgent":"muse-build/1.4.1 (non-interactive; linux-x86_64; build 35815c253477406321e3f5595195becc25e3907a)"}}} +{"dir":"out","msg":{"jsonrpc":"2.0","method":"initialized","params":{}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":2,"method":"session/start","params":{"commandId":"01a0ee49-ff6d-7f97-aa43-f75434037271","workspaceRoot":"/tmp/muse-phase0/repo","modelId":"meta/muse-spark-1.3","providerId":"meta","approvalMode":"onRequest","config":{"mcpServers":{"verification":{"transport":"stdio","command":"python3","args":["/tmp/muse-phase0/mcp-echo.py"],"framing":"lineDelimitedJson"}}}}}} +{"dir":"in","msg":{"emittedAtMs":1790704222151,"jsonrpc":"2.0","method":"session/started","params":{"session":{"activeTurnId":null,"approvalMode":{"lastCommandId":null,"mode":"onRequest","source":"startup"},"createdAt":"2026-09-29T17:50:22.071649Z","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee49-ff75-70e2-83fc-3169de40b80c/session.jsonl","providerId":"meta","sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","status":"idle","turnCount":0,"updatedAt":"2026-09-29T17:50:22.071653Z","workspaceRoot":"/tmp/muse-phase0/repo"}}}} +{"dir":"in","msg":{"id":2,"jsonrpc":"2.0","result":{"session":{"activeTurnId":null,"approvalMode":{"lastCommandId":null,"mode":"onRequest","source":"startup"},"createdAt":"2026-09-29T17:50:22.071649Z","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee49-ff75-70e2-83fc-3169de40b80c/session.jsonl","providerId":"meta","sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","status":"idle","turnCount":0,"updatedAt":"2026-09-29T17:50:22.071653Z","workspaceRoot":"/tmp/muse-phase0/repo"},"viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:1"}}} +{"dir":"in","msg":{"emittedAtMs":1790704222158,"jsonrpc":"2.0","method":"session/approvalModeChanged","params":{"clientName":"paseo_phase0","commandId":"01a0ee49-ff6d-7f97-aa43-f75434037271","mode":"onRequest","sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","source":"approvalReconfigure","sourceRange":{"first":{"id":"356fbc67-a2da-4c63-80e5-c40479aeecb7","sequence":5},"last":{"id":"356fbc67-a2da-4c63-80e5-c40479aeecb7","sequence":5},"stream":{"id":"01a0ee49-ff75-70e2-83fc-3169de40b80c","kind":"session"}},"viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:1"}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":3,"method":"turn/start","params":{"commandId":"01a0ee49-ffce-759b-a6f5-0a15a6623fa1","sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","input":[{"type":"text","text":"Call the tool mcp__verification__verification_echo with empty arguments {}, then reply with the marker it returns. This is a tool invocation test, do not use skills, shell, or subagents."}],"reasoningEffort":"low"}}} +{"dir":"in","msg":{"emittedAtMs":1790704222160,"jsonrpc":"2.0","method":"session/branchChanged","params":{"branch":"main","sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","sourceRange":{"first":{"id":"68cf6e02-29e6-4783-b564-e17feb742fae","sequence":24},"last":{"id":"68cf6e02-29e6-4783-b564-e17feb742fae","sequence":24},"stream":{"id":"01a0ee49-ff75-70e2-83fc-3169de40b80c","kind":"session"}},"vcs":"git","viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:2","workspaceRoot":"/tmp/muse-phase0/repo"}}} +{"dir":"in","msg":{"id":3,"jsonrpc":"2.0","result":{"commandId":"01a0ee49-ffce-759b-a6f5-0a15a6623fa1","disposition":"started","startedNewTurn":true,"status":"accepted","turnId":"01a0ee49-ffce-759b-a6f5-0a15a6623fa1"}}} +{"dir":"in","msg":{"emittedAtMs":1790704222168,"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","status":"running","viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:3"}}} +{"dir":"in","msg":{"emittedAtMs":1790704222169,"jsonrpc":"2.0","method":"turn/started","params":{"commandId":"01a0ee49-ffce-759b-a6f5-0a15a6623fa1","sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","sourceRange":{"first":{"id":"ae50c494-8740-4906-a3d1-27a0681f234c","sequence":26},"last":{"id":"ae50c494-8740-4906-a3d1-27a0681f234c","sequence":26},"stream":{"id":"01a0ee49-ff75-70e2-83fc-3169de40b80c","kind":"session"}},"turnId":"01a0ee49-ffce-759b-a6f5-0a15a6623fa1","viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:3"}}} +{"dir":"in","msg":{"emittedAtMs":1790704222169,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"commandId":"01a0ee49-ffce-759b-a6f5-0a15a6623fa1","itemId":"8b14c48f-e6c3-4b66-9357-62707a02c3a6","kind":"userMessage","recordedAt":"2026-09-29T17:50:22.161705Z","revision":1,"status":"completed","text":"Call the tool mcp__verification__verification_echo with empty arguments {}, then reply with the marker it returns. This is a tool invocation test, do not use skills, shell, or subagents.","turnId":"01a0ee49-ffce-759b-a6f5-0a15a6623fa1"},"sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","sourceRange":{"first":{"id":"ae50c494-8740-4906-a3d1-27a0681f234c","sequence":26},"last":{"id":"ae50c494-8740-4906-a3d1-27a0681f234c","sequence":26},"stream":{"id":"01a0ee49-ff75-70e2-83fc-3169de40b80c","kind":"session"}},"viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:4"}}} +{"dir":"in","msg":{"emittedAtMs":1790704222176,"jsonrpc":"2.0","method":"item/started","params":{"item":{"childSessionId":"5ca0b9af-8b82-4259-b8de-eedcf562beb1","fallbackText":"Reminder child session","generationId":1,"itemId":"950ac8c1-ae7d-4bb0-945a-8ce2e29117b6","kind":"reminderChild","recordedAt":"2026-09-29T17:50:22.176196Z","reminderAgentId":"skill-reminder","revision":1,"status":"inProgress","taskId":"01a0ee49-ffde-70f1-9cd1-38c8b29f0681","turnId":"01a0ee49-ffce-759b-a6f5-0a15a6623fa1"},"sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","sourceRange":{"first":{"id":"950ac8c1-ae7d-4bb0-945a-8ce2e29117b6","sequence":47},"last":{"id":"950ac8c1-ae7d-4bb0-945a-8ce2e29117b6","sequence":47},"stream":{"id":"01a0ee49-ff75-70e2-83fc-3169de40b80c","kind":"session"}},"viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:5"}}} +{"dir":"in","msg":{"emittedAtMs":1790704222180,"jsonrpc":"2.0","method":"session/listChanged","params":{"session":{"activeTurnId":"01a0ee49-ffce-759b-a6f5-0a15a6623fa1","createdAt":"2026-09-29T17:50:22.071649Z","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee49-ff75-70e2-83fc-3169de40b80c/session.jsonl","providerId":"meta","sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","status":"running","turnCount":0,"updatedAt":"2026-09-29T17:50:22.162618Z","workspaceRoot":"/tmp/muse-phase0/repo"}}}} +{"dir":"in","msg":{"emittedAtMs":1790704225869,"jsonrpc":"2.0","method":"session/tokenUsage","params":{"cumulative":{"outputTokens":89,"promptTokens":36599,"totalTokens":36688},"durationMs":3688,"finishReason":"tool_calls","modelId":"meta/muse-spark-1.3","promptTokens":36599,"sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","sourceRange":{"first":{"id":"96546ee0-6886-4ba5-828f-e1e02f68cfc6","sequence":63},"last":{"id":"96546ee0-6886-4ba5-828f-e1e02f68cfc6","sequence":63},"stream":{"id":"01a0ee49-ff75-70e2-83fc-3169de40b80c","kind":"session"}},"totalTokens":36688,"turnId":"01a0ee49-ffce-759b-a6f5-0a15a6623fa1","usage":{"cacheReadTokens":0,"cacheWriteTokens":0,"cachedTokens":0,"inputTokens":36599,"outputTokens":89,"reasoningTokens":26},"viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:6"}}} +{"dir":"in","msg":{"emittedAtMs":1790704225872,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"childSessionId":"5ca0b9af-8b82-4259-b8de-eedcf562beb1","fallbackText":"Reminder child session","generationId":1,"itemId":"950ac8c1-ae7d-4bb0-945a-8ce2e29117b6","kind":"reminderChild","recordedAt":"2026-09-29T17:50:25.872231Z","reminderAgentId":"skill-reminder","revision":2,"status":"completed","taskId":"01a0ee49-ffde-70f1-9cd1-38c8b29f0681","turnId":"01a0ee49-ffce-759b-a6f5-0a15a6623fa1"},"sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","sourceRange":{"first":{"id":"950ac8c1-ae7d-4bb0-945a-8ce2e29117b6","sequence":47},"last":{"id":"93323499-a386-4fb0-a957-7b853bf6ddac","sequence":71},"stream":{"id":"01a0ee49-ff75-70e2-83fc-3169de40b80c","kind":"session"}},"viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:7"}}} +{"dir":"in","msg":{"emittedAtMs":1790704225874,"jsonrpc":"2.0","method":"item/started","params":{"item":{"childSessionId":"b31cbc74-b9fb-4f55-9a83-16da4f98ff47","fallbackText":"Reminder child session","generationId":2,"itemId":"6bddd0b0-df0c-484e-b709-e90fedf82a8b","kind":"reminderChild","recordedAt":"2026-09-29T17:50:25.874457Z","reminderAgentId":"skill-reminder","revision":1,"status":"inProgress","taskId":"01a0ee4a-0e51-7c50-97ea-3c9a62bbeb66","turnId":"01a0ee49-ffce-759b-a6f5-0a15a6623fa1"},"sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","sourceRange":{"first":{"id":"6bddd0b0-df0c-484e-b709-e90fedf82a8b","sequence":80},"last":{"id":"6bddd0b0-df0c-484e-b709-e90fedf82a8b","sequence":80},"stream":{"id":"01a0ee49-ff75-70e2-83fc-3169de40b80c","kind":"session"}},"viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:8"}}} +{"dir":"in","msg":{"emittedAtMs":1790704231107,"jsonrpc":"2.0","method":"session/tokenUsage","params":{"cumulative":{"outputTokens":216,"promptTokens":73402,"totalTokens":73618},"durationMs":5229,"finishReason":"tool_calls","modelId":"meta/muse-spark-1.3","promptTokens":36803,"sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","sourceRange":{"first":{"id":"f2c46170-263a-4b44-99c9-4d71be6d8143","sequence":96},"last":{"id":"f2c46170-263a-4b44-99c9-4d71be6d8143","sequence":96},"stream":{"id":"01a0ee49-ff75-70e2-83fc-3169de40b80c","kind":"session"}},"totalTokens":36930,"turnId":"01a0ee49-ffce-759b-a6f5-0a15a6623fa1","usage":{"cacheReadTokens":36593,"cacheWriteTokens":0,"cachedTokens":36593,"inputTokens":36803,"outputTokens":127,"reasoningTokens":63},"viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:9"}}} +{"dir":"in","msg":{"emittedAtMs":1790704234936,"jsonrpc":"2.0","method":"session/tokenUsage","params":{"cumulative":{"outputTokens":344,"promptTokens":110447,"totalTokens":110791},"durationMs":3820,"finishReason":"tool_calls","modelId":"meta/muse-spark-1.3","promptTokens":37045,"sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","sourceRange":{"first":{"id":"4c9d9d20-7cff-4f69-83b9-8ee9f3e3eb3b","sequence":121},"last":{"id":"4c9d9d20-7cff-4f69-83b9-8ee9f3e3eb3b","sequence":121},"stream":{"id":"01a0ee49-ff75-70e2-83fc-3169de40b80c","kind":"session"}},"totalTokens":37173,"turnId":"01a0ee49-ffce-759b-a6f5-0a15a6623fa1","usage":{"cacheReadTokens":36721,"cacheWriteTokens":0,"cachedTokens":36721,"inputTokens":37045,"outputTokens":128,"reasoningTokens":64},"viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:10"}}} +{"dir":"in","msg":{"emittedAtMs":1790704234941,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"childSessionId":"b31cbc74-b9fb-4f55-9a83-16da4f98ff47","fallbackText":"Reminder child session","generationId":2,"itemId":"6bddd0b0-df0c-484e-b709-e90fedf82a8b","kind":"reminderChild","recordedAt":"2026-09-29T17:50:34.941058Z","reminderAgentId":"skill-reminder","revision":2,"status":"completed","taskId":"01a0ee4a-0e51-7c50-97ea-3c9a62bbeb66","turnId":"01a0ee49-ffce-759b-a6f5-0a15a6623fa1"},"sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","sourceRange":{"first":{"id":"6bddd0b0-df0c-484e-b709-e90fedf82a8b","sequence":80},"last":{"id":"efa65401-33e1-4953-870a-b3d50f38201c","sequence":129},"stream":{"id":"01a0ee49-ff75-70e2-83fc-3169de40b80c","kind":"session"}},"viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:11"}}} +{"dir":"in","msg":{"emittedAtMs":1790704234943,"jsonrpc":"2.0","method":"item/started","params":{"item":{"childSessionId":"4a7bc4ed-8a9d-4c64-bc58-5974a231ec3e","fallbackText":"Reminder child session","generationId":3,"itemId":"14dce19f-806f-49ea-a642-27f1add14e6b","kind":"reminderChild","recordedAt":"2026-09-29T17:50:34.943414Z","reminderAgentId":"skill-reminder","revision":1,"status":"inProgress","taskId":"01a0ee4a-31be-77d3-97a0-b02176f1901a","turnId":"01a0ee49-ffce-759b-a6f5-0a15a6623fa1"},"sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","sourceRange":{"first":{"id":"14dce19f-806f-49ea-a642-27f1add14e6b","sequence":138},"last":{"id":"14dce19f-806f-49ea-a642-27f1add14e6b","sequence":138},"stream":{"id":"01a0ee49-ff75-70e2-83fc-3169de40b80c","kind":"session"}},"viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:12"}}} +{"dir":"in","msg":{"emittedAtMs":1790704238997,"jsonrpc":"2.0","method":"session/tokenUsage","params":{"cumulative":{"outputTokens":490,"promptTokens":147735,"totalTokens":148225},"durationMs":4049,"finishReason":"tool_calls","modelId":"meta/muse-spark-1.3","promptTokens":37288,"sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","sourceRange":{"first":{"id":"fa304ed0-f74c-4ed3-9cec-fc6ffa00ba8a","sequence":154},"last":{"id":"fa304ed0-f74c-4ed3-9cec-fc6ffa00ba8a","sequence":154},"stream":{"id":"01a0ee49-ff75-70e2-83fc-3169de40b80c","kind":"session"}},"totalTokens":37434,"turnId":"01a0ee49-ffce-759b-a6f5-0a15a6623fa1","usage":{"cacheReadTokens":0,"cacheWriteTokens":0,"cachedTokens":0,"inputTokens":37288,"outputTokens":146,"reasoningTokens":73},"viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:13"}}} +{"dir":"in","msg":{"emittedAtMs":1790704245514,"jsonrpc":"2.0","method":"session/tokenUsage","params":{"cumulative":{"outputTokens":731,"promptTokens":185285,"totalTokens":186016},"durationMs":6492,"finishReason":"tool_calls","modelId":"meta/muse-spark-1.3","promptTokens":37550,"sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","sourceRange":{"first":{"id":"72cddfc7-d0e3-408f-9d27-adf1b38dbe80","sequence":179},"last":{"id":"72cddfc7-d0e3-408f-9d27-adf1b38dbe80","sequence":179},"stream":{"id":"01a0ee49-ff75-70e2-83fc-3169de40b80c","kind":"session"}},"totalTokens":37791,"turnId":"01a0ee49-ffce-759b-a6f5-0a15a6623fa1","usage":{"cacheReadTokens":37233,"cacheWriteTokens":0,"cachedTokens":37233,"inputTokens":37550,"outputTokens":241,"reasoningTokens":177},"viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:14"}}} +{"dir":"in","msg":{"emittedAtMs":1790704245518,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"childSessionId":"4a7bc4ed-8a9d-4c64-bc58-5974a231ec3e","fallbackText":"Reminder child session","generationId":3,"itemId":"14dce19f-806f-49ea-a642-27f1add14e6b","kind":"reminderChild","recordedAt":"2026-09-29T17:50:45.518533Z","reminderAgentId":"skill-reminder","revision":2,"status":"completed","taskId":"01a0ee4a-31be-77d3-97a0-b02176f1901a","turnId":"01a0ee49-ffce-759b-a6f5-0a15a6623fa1"},"sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","sourceRange":{"first":{"id":"14dce19f-806f-49ea-a642-27f1add14e6b","sequence":138},"last":{"id":"434082ce-4c9c-435e-b36a-86e5ea22bab2","sequence":187},"stream":{"id":"01a0ee49-ff75-70e2-83fc-3169de40b80c","kind":"session"}},"viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:15"}}} +{"dir":"in","msg":{"emittedAtMs":1790704245521,"jsonrpc":"2.0","method":"item/started","params":{"item":{"childSessionId":"7d4ba2ac-40d4-43ef-a51c-91a99ba79385","fallbackText":"Reminder child session","generationId":4,"itemId":"c6b536ce-e60f-4b52-b2de-3da223242d7e","kind":"reminderChild","recordedAt":"2026-09-29T17:50:45.520815Z","reminderAgentId":"skill-reminder","revision":1,"status":"inProgress","taskId":"01a0ee4a-5b0f-7590-9d47-9da4390df674","turnId":"01a0ee49-ffce-759b-a6f5-0a15a6623fa1"},"sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","sourceRange":{"first":{"id":"c6b536ce-e60f-4b52-b2de-3da223242d7e","sequence":196},"last":{"id":"c6b536ce-e60f-4b52-b2de-3da223242d7e","sequence":196},"stream":{"id":"01a0ee49-ff75-70e2-83fc-3169de40b80c","kind":"session"}},"viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:16"}}} +{"dir":"in","msg":{"emittedAtMs":1790704250703,"jsonrpc":"2.0","method":"item/started","params":{"item":{"itemId":"1b23c527-97c0-419e-b58d-8d503292c6da","kind":"reasoning","revision":1,"status":"inProgress","summary":[""],"turnId":"01a0ee49-ffce-759b-a6f5-0a15a6623fa1"},"sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:16.1"}}} +{"dir":"in","msg":{"emittedAtMs":1790704250703,"jsonrpc":"2.0","method":"item/delta","params":{"delta":"Correcting tool recipient format to use default namespace and single call per message.","field":"summary.0","itemId":"1b23c527-97c0-419e-b58d-8d503292c6da","sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:16.2"}}} +{"dir":"in","msg":{"emittedAtMs":1790704255209,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"itemId":"1b23c527-97c0-419e-b58d-8d503292c6da","kind":"reasoning","providerItemId":"rs_6abbfa7637a483bc11064bbe:rs_01a0ee4a5fe17414beae1c6817909f9e","recordedAt":"2026-09-29T17:50:55.209309Z","revision":2,"status":"completed","summary":["Correcting tool recipient format to use default namespace and single call per message."],"turnId":"01a0ee49-ffce-759b-a6f5-0a15a6623fa1"},"sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","sourceRange":{"first":{"id":"12a3e665-f587-4433-bc7b-0fc35e721177","sequence":209},"last":{"id":"12a3e665-f587-4433-bc7b-0fc35e721177","sequence":209},"stream":{"id":"01a0ee49-ff75-70e2-83fc-3169de40b80c","kind":"session"}},"viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:17"}}} +{"dir":"in","msg":{"emittedAtMs":1790704255767,"jsonrpc":"2.0","method":"session/tokenUsage","params":{"cumulative":{"outputTokens":1235,"promptTokens":223192,"totalTokens":224427},"durationMs":10242,"finishReason":"tool_calls","modelId":"meta/muse-spark-1.3","promptTokens":37907,"sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","sourceRange":{"first":{"id":"45183575-0909-4350-9042-34bcd986d704","sequence":214},"last":{"id":"45183575-0909-4350-9042-34bcd986d704","sequence":214},"stream":{"id":"01a0ee49-ff75-70e2-83fc-3169de40b80c","kind":"session"}},"totalTokens":38411,"turnId":"01a0ee49-ffce-759b-a6f5-0a15a6623fa1","usage":{"cacheReadTokens":37489,"cacheWriteTokens":0,"cachedTokens":37489,"inputTokens":37907,"outputTokens":504,"reasoningTokens":417},"viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:18"}}} +{"dir":"in","msg":{"emittedAtMs":1790704255771,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"childSessionId":"7d4ba2ac-40d4-43ef-a51c-91a99ba79385","fallbackText":"Reminder child session","generationId":4,"itemId":"c6b536ce-e60f-4b52-b2de-3da223242d7e","kind":"reminderChild","recordedAt":"2026-09-29T17:50:55.770844Z","reminderAgentId":"skill-reminder","revision":2,"status":"completed","taskId":"01a0ee4a-5b0f-7590-9d47-9da4390df674","turnId":"01a0ee49-ffce-759b-a6f5-0a15a6623fa1"},"sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","sourceRange":{"first":{"id":"c6b536ce-e60f-4b52-b2de-3da223242d7e","sequence":196},"last":{"id":"706650ff-b21b-4d98-ad2a-bd2793833c08","sequence":222},"stream":{"id":"01a0ee49-ff75-70e2-83fc-3169de40b80c","kind":"session"}},"viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:19"}}} +{"dir":"in","msg":{"emittedAtMs":1790704255773,"jsonrpc":"2.0","method":"item/started","params":{"item":{"childSessionId":"03cc87ac-f323-4150-9cf2-c17c1e5a10d5","fallbackText":"Reminder child session","generationId":5,"itemId":"6cf23922-e14c-40a5-ac69-7750a1f53dfb","kind":"reminderChild","recordedAt":"2026-09-29T17:50:55.77316Z","reminderAgentId":"skill-reminder","revision":1,"status":"inProgress","taskId":"01a0ee4a-831b-7291-990a-db63e7fb951a","turnId":"01a0ee49-ffce-759b-a6f5-0a15a6623fa1"},"sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","sourceRange":{"first":{"id":"6cf23922-e14c-40a5-ac69-7750a1f53dfb","sequence":231},"last":{"id":"6cf23922-e14c-40a5-ac69-7750a1f53dfb","sequence":231},"stream":{"id":"01a0ee49-ff75-70e2-83fc-3169de40b80c","kind":"session"}},"viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:20"}}} +{"dir":"in","msg":{"emittedAtMs":1790704260441,"jsonrpc":"2.0","method":"item/started","params":{"item":{"itemId":"54fea1ca-2a35-46ca-80be-ae6bbe8640e2","kind":"reasoning","revision":1,"status":"inProgress","summary":[""],"turnId":"01a0ee49-ffce-759b-a6f5-0a15a6623fa1"},"sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:20.1"}}} +{"dir":"in","msg":{"emittedAtMs":1790704260441,"jsonrpc":"2.0","method":"item/delta","params":{"delta":"Adjusting the tool call recipient format from muse to default mcp verification namespace.","field":"summary.0","itemId":"54fea1ca-2a35-46ca-80be-ae6bbe8640e2","sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:20.2"}}} +{"dir":"in","msg":{"emittedAtMs":1790704264663,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"itemId":"54fea1ca-2a35-46ca-80be-ae6bbe8640e2","kind":"reasoning","providerItemId":"rs_6abbfa807d7a3c13a2b24a99:rs_01a0ee4a87827736b5af6b96ba7ee8f0","recordedAt":"2026-09-29T17:51:04.662652Z","revision":2,"status":"completed","summary":["Adjusting the tool call recipient format from muse to default mcp verification namespace."],"turnId":"01a0ee49-ffce-759b-a6f5-0a15a6623fa1"},"sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","sourceRange":{"first":{"id":"24295be2-fb95-4aa6-9ed2-eddd54703f68","sequence":244},"last":{"id":"24295be2-fb95-4aa6-9ed2-eddd54703f68","sequence":244},"stream":{"id":"01a0ee49-ff75-70e2-83fc-3169de40b80c","kind":"session"}},"viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:21"}}} +{"dir":"in","msg":{"emittedAtMs":1790704265001,"jsonrpc":"2.0","method":"session/tokenUsage","params":{"cumulative":{"outputTokens":1786,"promptTokens":261700,"totalTokens":263486},"durationMs":9224,"finishReason":"tool_calls","modelId":"meta/muse-spark-1.3","promptTokens":38508,"sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","sourceRange":{"first":{"id":"ea636237-3757-4ab4-afea-fc6cf3edb4e0","sequence":249},"last":{"id":"ea636237-3757-4ab4-afea-fc6cf3edb4e0","sequence":249},"stream":{"id":"01a0ee49-ff75-70e2-83fc-3169de40b80c","kind":"session"}},"totalTokens":39059,"turnId":"01a0ee49-ffce-759b-a6f5-0a15a6623fa1","usage":{"cacheReadTokens":0,"cacheWriteTokens":0,"cachedTokens":0,"inputTokens":38508,"outputTokens":551,"reasoningTokens":461},"viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:22"}}} +{"dir":"in","msg":{"emittedAtMs":1790704265005,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"childSessionId":"03cc87ac-f323-4150-9cf2-c17c1e5a10d5","fallbackText":"Reminder child session","generationId":5,"itemId":"6cf23922-e14c-40a5-ac69-7750a1f53dfb","kind":"reminderChild","recordedAt":"2026-09-29T17:51:05.004993Z","reminderAgentId":"skill-reminder","revision":2,"status":"completed","taskId":"01a0ee4a-831b-7291-990a-db63e7fb951a","turnId":"01a0ee49-ffce-759b-a6f5-0a15a6623fa1"},"sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","sourceRange":{"first":{"id":"6cf23922-e14c-40a5-ac69-7750a1f53dfb","sequence":231},"last":{"id":"22ed2499-d899-4c63-84c8-22a8d6672824","sequence":257},"stream":{"id":"01a0ee49-ff75-70e2-83fc-3169de40b80c","kind":"session"}},"viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:23"}}} +{"dir":"in","msg":{"emittedAtMs":1790704265007,"jsonrpc":"2.0","method":"item/started","params":{"item":{"childSessionId":"ac62eb71-a002-4270-8e3a-25ff0f0773f1","fallbackText":"Reminder child session","generationId":6,"itemId":"5b6224bb-3a17-402e-9422-43716854c99f","kind":"reminderChild","recordedAt":"2026-09-29T17:51:05.007287Z","reminderAgentId":"skill-reminder","revision":1,"status":"inProgress","taskId":"01a0ee4a-a72d-7f62-9c0e-2af665756771","turnId":"01a0ee49-ffce-759b-a6f5-0a15a6623fa1"},"sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","sourceRange":{"first":{"id":"5b6224bb-3a17-402e-9422-43716854c99f","sequence":266},"last":{"id":"5b6224bb-3a17-402e-9422-43716854c99f","sequence":266},"stream":{"id":"01a0ee49-ff75-70e2-83fc-3169de40b80c","kind":"session"}},"viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:24"}}} +{"dir":"in","msg":{"emittedAtMs":1790704269558,"jsonrpc":"2.0","method":"item/started","params":{"item":{"itemId":"d27db2ad-4734-441c-9775-a6b6089bdfb5","kind":"reasoning","revision":1,"status":"inProgress","summary":[""],"turnId":"01a0ee49-ffce-759b-a6f5-0a15a6623fa1"},"sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:24.1"}}} +{"dir":"in","msg":{"emittedAtMs":1790704269558,"jsonrpc":"2.0","method":"item/delta","params":{"delta":"Correcting the tool invocation name to mcp__verification__verification_echo after misrouting.","field":"summary.0","itemId":"d27db2ad-4734-441c-9775-a6b6089bdfb5","sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:24.2"}}} +{"dir":"in","msg":{"emittedAtMs":1790704269788,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"itemId":"d27db2ad-4734-441c-9775-a6b6089bdfb5","kind":"reasoning","providerItemId":"rs_6abbfa89f29054f8cb46439b:rs_01a0ee4aaa0a726fb9de1f2874785d88","recordedAt":"2026-09-29T17:51:09.78848Z","revision":2,"status":"completed","summary":["Correcting the tool invocation name to mcp__verification__verification_echo after misrouting."],"turnId":"01a0ee49-ffce-759b-a6f5-0a15a6623fa1"},"sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","sourceRange":{"first":{"id":"0e03b0b6-7bbe-40e8-bfbb-232ef7b07597","sequence":279},"last":{"id":"0e03b0b6-7bbe-40e8-bfbb-232ef7b07597","sequence":279},"stream":{"id":"01a0ee49-ff75-70e2-83fc-3169de40b80c","kind":"session"}},"viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:25"}}} +{"dir":"in","msg":{"emittedAtMs":1790704269826,"jsonrpc":"2.0","method":"session/tokenUsage","params":{"cumulative":{"outputTokens":2069,"promptTokens":300855,"totalTokens":302924},"durationMs":4815,"finishReason":"tool_calls","modelId":"meta/muse-spark-1.3","promptTokens":39155,"sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","sourceRange":{"first":{"id":"9c74fed5-eaf7-41c7-8e28-1f12e4c1575c","sequence":283},"last":{"id":"9c74fed5-eaf7-41c7-8e28-1f12e4c1575c","sequence":283},"stream":{"id":"01a0ee49-ff75-70e2-83fc-3169de40b80c","kind":"session"}},"totalTokens":39438,"turnId":"01a0ee49-ffce-759b-a6f5-0a15a6623fa1","usage":{"cacheReadTokens":38385,"cacheWriteTokens":0,"cachedTokens":38385,"inputTokens":39155,"outputTokens":283,"reasoningTokens":215},"viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:26"}}} +{"dir":"in","msg":{"emittedAtMs":1790704273632,"jsonrpc":"2.0","method":"item/started","params":{"item":{"itemId":"5f0a933d-76c4-45f3-a7eb-580d1dbe6fd9","kind":"reasoning","revision":1,"status":"inProgress","summary":[""],"turnId":"01a0ee49-ffce-759b-a6f5-0a15a6623fa1"},"sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:26.1"}}} +{"dir":"in","msg":{"emittedAtMs":1790704273632,"jsonrpc":"2.0","method":"item/delta","params":{"delta":"Identifying a recipient formatting error and correcting the tool call header to use the valid default namespace.","field":"summary.0","itemId":"5f0a933d-76c4-45f3-a7eb-580d1dbe6fd9","sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:26.2"}}} +{"dir":"in","msg":{"emittedAtMs":1790704276401,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"itemId":"5f0a933d-76c4-45f3-a7eb-580d1dbe6fd9","kind":"reasoning","providerItemId":"rs_6abbfa8e85b93bd6201545ef:rs_01a0ee4abccb70efa27f3573318309f6","recordedAt":"2026-09-29T17:51:16.401006Z","revision":2,"status":"completed","summary":["Identifying a recipient formatting error and correcting the tool call header to use the valid default namespace."],"turnId":"01a0ee49-ffce-759b-a6f5-0a15a6623fa1"},"sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","sourceRange":{"first":{"id":"47a12323-0ed4-4b27-8f03-6b66b8895930","sequence":305},"last":{"id":"47a12323-0ed4-4b27-8f03-6b66b8895930","sequence":305},"stream":{"id":"01a0ee49-ff75-70e2-83fc-3169de40b80c","kind":"session"}},"viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:27"}}} +{"dir":"in","msg":{"emittedAtMs":1790704276673,"jsonrpc":"2.0","method":"session/tokenUsage","params":{"cumulative":{"outputTokens":2480,"promptTokens":340387,"totalTokens":342867},"durationMs":6837,"finishReason":"tool_calls","modelId":"meta/muse-spark-1.3","promptTokens":39532,"sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","sourceRange":{"first":{"id":"eda82807-0801-4ba3-bff5-d05c277acf07","sequence":310},"last":{"id":"eda82807-0801-4ba3-bff5-d05c277acf07","sequence":310},"stream":{"id":"01a0ee49-ff75-70e2-83fc-3169de40b80c","kind":"session"}},"totalTokens":39943,"turnId":"01a0ee49-ffce-759b-a6f5-0a15a6623fa1","usage":{"cacheReadTokens":39153,"cacheWriteTokens":0,"cachedTokens":39153,"inputTokens":39532,"outputTokens":411,"reasoningTokens":322},"viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:28"}}} +{"dir":"in","msg":{"emittedAtMs":1790704276677,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"childSessionId":"ac62eb71-a002-4270-8e3a-25ff0f0773f1","fallbackText":"Reminder child session","generationId":6,"itemId":"5b6224bb-3a17-402e-9422-43716854c99f","kind":"reminderChild","recordedAt":"2026-09-29T17:51:16.677407Z","reminderAgentId":"skill-reminder","revision":2,"status":"completed","taskId":"01a0ee4a-a72d-7f62-9c0e-2af665756771","turnId":"01a0ee49-ffce-759b-a6f5-0a15a6623fa1"},"sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","sourceRange":{"first":{"id":"5b6224bb-3a17-402e-9422-43716854c99f","sequence":266},"last":{"id":"547b3d14-3fc5-4e6d-af3e-c06d7489c3e2","sequence":318},"stream":{"id":"01a0ee49-ff75-70e2-83fc-3169de40b80c","kind":"session"}},"viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:29"}}} +{"dir":"in","msg":{"emittedAtMs":1790704276680,"jsonrpc":"2.0","method":"item/started","params":{"item":{"childSessionId":"10fc93c2-00df-4c06-8faa-85f1ca5d0c4a","fallbackText":"Reminder child session","generationId":7,"itemId":"ecfec1a1-50f0-41b2-a182-02c7abba169a","kind":"reminderChild","recordedAt":"2026-09-29T17:51:16.679786Z","reminderAgentId":"skill-reminder","revision":1,"status":"inProgress","taskId":"01a0ee4a-d4c6-7de2-9c72-4bd8e0679a9b","turnId":"01a0ee49-ffce-759b-a6f5-0a15a6623fa1"},"sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","sourceRange":{"first":{"id":"ecfec1a1-50f0-41b2-a182-02c7abba169a","sequence":327},"last":{"id":"ecfec1a1-50f0-41b2-a182-02c7abba169a","sequence":327},"stream":{"id":"01a0ee49-ff75-70e2-83fc-3169de40b80c","kind":"session"}},"viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:30"}}} +{"dir":"in","msg":{"emittedAtMs":1790704282675,"jsonrpc":"2.0","method":"item/started","params":{"item":{"itemId":"e03fcbc6-d28b-4782-b601-e5ab3173f0b8","kind":"reasoning","revision":1,"status":"inProgress","summary":[""],"turnId":"01a0ee49-ffce-759b-a6f5-0a15a6623fa1"},"sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:30.1"}}} +{"dir":"in","msg":{"emittedAtMs":1790704282675,"jsonrpc":"2.0","method":"item/delta","params":{"delta":"Adjusting the recipient identifier to match the session tool name by changing the to= field.","field":"summary.0","itemId":"e03fcbc6-d28b-4782-b601-e5ab3173f0b8","sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:30.2"}}} +{"dir":"in","msg":{"emittedAtMs":1790704290578,"jsonrpc":"2.0","method":"item/delta","params":{"delta":"Switching the invoke name to the correct tool function and emitting the proper call format.","field":"summary.0","itemId":"e03fcbc6-d28b-4782-b601-e5ab3173f0b8","sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:30.3"}}} +{"dir":"in","msg":{"emittedAtMs":1790704293289,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"itemId":"e03fcbc6-d28b-4782-b601-e5ab3173f0b8","kind":"reasoning","providerItemId":"rs_6abbfa957c5905a580234079:rs_01a0ee4ad974758a8d721d5433593d36","recordedAt":"2026-09-29T17:51:33.288684Z","revision":2,"status":"completed","summary":["Adjusting the recipient identifier to match the session tool name by changing the to= field.Switching the invoke name to the correct tool function and emitting the proper call format."],"turnId":"01a0ee49-ffce-759b-a6f5-0a15a6623fa1"},"sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","sourceRange":{"first":{"id":"e623e93f-e459-4eda-8f0d-13e66fb6ced9","sequence":341},"last":{"id":"e623e93f-e459-4eda-8f0d-13e66fb6ced9","sequence":341},"stream":{"id":"01a0ee49-ff75-70e2-83fc-3169de40b80c","kind":"session"}},"viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:31"}}} +{"dir":"in","msg":{"emittedAtMs":1790704293385,"jsonrpc":"2.0","method":"session/tokenUsage","params":{"cumulative":{"outputTokens":3250,"promptTokens":380424,"totalTokens":383674},"durationMs":16700,"finishReason":"tool_calls","modelId":"meta/muse-spark-1.3","promptTokens":40037,"sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","sourceRange":{"first":{"id":"e1f583cd-fa38-4a7e-b877-09481a25ff97","sequence":345},"last":{"id":"e1f583cd-fa38-4a7e-b877-09481a25ff97","sequence":345},"stream":{"id":"01a0ee49-ff75-70e2-83fc-3169de40b80c","kind":"session"}},"totalTokens":40807,"turnId":"01a0ee49-ffce-759b-a6f5-0a15a6623fa1","usage":{"cacheReadTokens":39409,"cacheWriteTokens":0,"cachedTokens":39409,"inputTokens":40037,"outputTokens":770,"reasoningTokens":680},"viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:32"}}} +{"dir":"in","msg":{"emittedAtMs":1790704293389,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"childSessionId":"10fc93c2-00df-4c06-8faa-85f1ca5d0c4a","fallbackText":"Reminder child session","generationId":7,"itemId":"ecfec1a1-50f0-41b2-a182-02c7abba169a","kind":"reminderChild","recordedAt":"2026-09-29T17:51:33.389095Z","reminderAgentId":"skill-reminder","revision":2,"status":"completed","taskId":"01a0ee4a-d4c6-7de2-9c72-4bd8e0679a9b","turnId":"01a0ee49-ffce-759b-a6f5-0a15a6623fa1"},"sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","sourceRange":{"first":{"id":"ecfec1a1-50f0-41b2-a182-02c7abba169a","sequence":327},"last":{"id":"200e6040-860b-4734-82d8-b0ac53e2f6e3","sequence":353},"stream":{"id":"01a0ee49-ff75-70e2-83fc-3169de40b80c","kind":"session"}},"viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:33"}}} +{"dir":"in","msg":{"emittedAtMs":1790704293391,"jsonrpc":"2.0","method":"item/started","params":{"item":{"childSessionId":"ee0d6f9d-3224-4269-9470-453691b7b36f","fallbackText":"Reminder child session","generationId":8,"itemId":"d758af60-1585-4c7c-9e18-c05f14212e0b","kind":"reminderChild","recordedAt":"2026-09-29T17:51:33.391427Z","reminderAgentId":"skill-reminder","revision":1,"status":"inProgress","taskId":"01a0ee4b-160e-7120-b2a1-6d7c9e2a15c5","turnId":"01a0ee49-ffce-759b-a6f5-0a15a6623fa1"},"sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","sourceRange":{"first":{"id":"d758af60-1585-4c7c-9e18-c05f14212e0b","sequence":362},"last":{"id":"d758af60-1585-4c7c-9e18-c05f14212e0b","sequence":362},"stream":{"id":"01a0ee49-ff75-70e2-83fc-3169de40b80c","kind":"session"}},"viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:34"}}} +{"dir":"in","msg":{"emittedAtMs":1790704297931,"jsonrpc":"2.0","method":"item/started","params":{"item":{"itemId":"bc432b76-93a1-4890-ab02-a7849f94b2d1","kind":"reasoning","revision":1,"status":"inProgress","summary":[""],"turnId":"01a0ee49-ffce-759b-a6f5-0a15a6623fa1"},"sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:34.1"}}} +{"dir":"in","msg":{"emittedAtMs":1790704297931,"jsonrpc":"2.0","method":"item/delta","params":{"delta":"Analyzing why the recipient is fixed and considering alternative addressing methods.","field":"summary.0","itemId":"bc432b76-93a1-4890-ab02-a7849f94b2d1","sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:34.2"}}} +{"dir":"in","msg":{"emittedAtMs":1790704302395,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"itemId":"bc432b76-93a1-4890-ab02-a7849f94b2d1","kind":"reasoning","providerItemId":"rs_6abbfaa68c4866b6e8014972:rs_01a0ee4b1b82768992c4b4dbd733e67f","recordedAt":"2026-09-29T17:51:42.395133Z","revision":2,"status":"completed","summary":["Analyzing why the recipient is fixed and considering alternative addressing methods."],"turnId":"01a0ee49-ffce-759b-a6f5-0a15a6623fa1"},"sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","sourceRange":{"first":{"id":"ab6d3e6a-49e7-4857-bb08-8059aa7707ee","sequence":375},"last":{"id":"ab6d3e6a-49e7-4857-bb08-8059aa7707ee","sequence":375},"stream":{"id":"01a0ee49-ff75-70e2-83fc-3169de40b80c","kind":"session"}},"viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:35"}}} +{"dir":"in","msg":{"emittedAtMs":1790704302618,"jsonrpc":"2.0","method":"session/tokenUsage","params":{"cumulative":{"outputTokens":3825,"promptTokens":421303,"totalTokens":425128},"durationMs":9222,"finishReason":"tool_calls","modelId":"meta/muse-spark-1.3","promptTokens":40879,"sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","sourceRange":{"first":{"id":"c2f6467e-c1db-46dc-9fa6-fa749ac33929","sequence":380},"last":{"id":"c2f6467e-c1db-46dc-9fa6-fa749ac33929","sequence":380},"stream":{"id":"01a0ee49-ff75-70e2-83fc-3169de40b80c","kind":"session"}},"totalTokens":41454,"turnId":"01a0ee49-ffce-759b-a6f5-0a15a6623fa1","usage":{"cacheReadTokens":39921,"cacheWriteTokens":0,"cachedTokens":39921,"inputTokens":40879,"outputTokens":575,"reasoningTokens":491},"viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:36"}}} +{"dir":"in","msg":{"emittedAtMs":1790704302621,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"childSessionId":"ee0d6f9d-3224-4269-9470-453691b7b36f","fallbackText":"Reminder child session","generationId":8,"itemId":"d758af60-1585-4c7c-9e18-c05f14212e0b","kind":"reminderChild","recordedAt":"2026-09-29T17:51:42.621717Z","reminderAgentId":"skill-reminder","revision":2,"status":"completed","taskId":"01a0ee4b-160e-7120-b2a1-6d7c9e2a15c5","turnId":"01a0ee49-ffce-759b-a6f5-0a15a6623fa1"},"sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","sourceRange":{"first":{"id":"d758af60-1585-4c7c-9e18-c05f14212e0b","sequence":362},"last":{"id":"dfc529d7-ffd6-4257-b5db-09d38024dd59","sequence":388},"stream":{"id":"01a0ee49-ff75-70e2-83fc-3169de40b80c","kind":"session"}},"viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:37"}}} +{"dir":"in","msg":{"emittedAtMs":1790704302624,"jsonrpc":"2.0","method":"item/started","params":{"item":{"childSessionId":"269f5880-80ac-41f9-b348-61a343176893","fallbackText":"Reminder child session","generationId":9,"itemId":"e77dc995-fea7-42ef-989f-8848321157ea","kind":"reminderChild","recordedAt":"2026-09-29T17:51:42.624086Z","reminderAgentId":"skill-reminder","revision":1,"status":"inProgress","taskId":"01a0ee4b-3a1e-7463-bccf-274eaea2d105","turnId":"01a0ee49-ffce-759b-a6f5-0a15a6623fa1"},"sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","sourceRange":{"first":{"id":"e77dc995-fea7-42ef-989f-8848321157ea","sequence":397},"last":{"id":"e77dc995-fea7-42ef-989f-8848321157ea","sequence":397},"stream":{"id":"01a0ee49-ff75-70e2-83fc-3169de40b80c","kind":"session"}},"viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:38"}}} +{"dir":"in","msg":{"emittedAtMs":1790704307256,"jsonrpc":"2.0","method":"item/started","params":{"item":{"itemId":"7cb4a150-6b50-4ba5-b8b7-cd7457bea6a7","kind":"reasoning","revision":1,"status":"inProgress","summary":[""],"turnId":"01a0ee49-ffce-759b-a6f5-0a15a6623fa1"},"sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:38.1"}}} +{"dir":"in","msg":{"emittedAtMs":1790704307256,"jsonrpc":"2.0","method":"item/delta","params":{"delta":"Breaking the repeated incorrect tool-call pattern by explicitly typing the default namespace function name character by character.","field":"summary.0","itemId":"7cb4a150-6b50-4ba5-b8b7-cd7457bea6a7","sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:38.2"}}} +{"dir":"in","msg":{"emittedAtMs":1790704310664,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"itemId":"7cb4a150-6b50-4ba5-b8b7-cd7457bea6a7","kind":"reasoning","providerItemId":"rs_6abbfaaf5ff9ac4cd0b5413b:rs_01a0ee4b3cf17487a3f42807a4cf2d32","recordedAt":"2026-09-29T17:51:50.664198Z","revision":2,"status":"completed","summary":["Breaking the repeated incorrect tool-call pattern by explicitly typing the default namespace function name character by character."],"turnId":"01a0ee49-ffce-759b-a6f5-0a15a6623fa1"},"sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","sourceRange":{"first":{"id":"56692ccd-4cbe-493e-944d-5e03707a8832","sequence":410},"last":{"id":"56692ccd-4cbe-493e-944d-5e03707a8832","sequence":410},"stream":{"id":"01a0ee49-ff75-70e2-83fc-3169de40b80c","kind":"session"}},"viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:39"}}} +{"dir":"in","msg":{"emittedAtMs":1790704310949,"jsonrpc":"2.0","method":"session/tokenUsage","params":{"cumulative":{"outputTokens":4130,"promptTokens":462855,"totalTokens":466985},"durationMs":8320,"finishReason":"tool_calls","modelId":"meta/muse-spark-1.3","promptTokens":41552,"sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","sourceRange":{"first":{"id":"fc809e7f-738f-4fb2-8990-d231a92b3dee","sequence":414},"last":{"id":"fc809e7f-738f-4fb2-8990-d231a92b3dee","sequence":414},"stream":{"id":"01a0ee49-ff75-70e2-83fc-3169de40b80c","kind":"session"}},"totalTokens":41857,"turnId":"01a0ee49-ffce-759b-a6f5-0a15a6623fa1","usage":{"cacheReadTokens":40817,"cacheWriteTokens":0,"cachedTokens":40817,"inputTokens":41552,"outputTokens":305,"reasoningTokens":236},"viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:40"}}} +{"dir":"in","msg":{"emittedAtMs":1790704310953,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"childSessionId":"269f5880-80ac-41f9-b348-61a343176893","fallbackText":"Reminder child session","generationId":9,"itemId":"e77dc995-fea7-42ef-989f-8848321157ea","kind":"reminderChild","recordedAt":"2026-09-29T17:51:50.953035Z","reminderAgentId":"skill-reminder","revision":2,"status":"completed","taskId":"01a0ee4b-3a1e-7463-bccf-274eaea2d105","turnId":"01a0ee49-ffce-759b-a6f5-0a15a6623fa1"},"sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","sourceRange":{"first":{"id":"e77dc995-fea7-42ef-989f-8848321157ea","sequence":397},"last":{"id":"24356eb4-6215-4702-8e43-e50d9b975511","sequence":422},"stream":{"id":"01a0ee49-ff75-70e2-83fc-3169de40b80c","kind":"session"}},"viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:41"}}} +{"dir":"in","msg":{"emittedAtMs":1790704310955,"jsonrpc":"2.0","method":"item/started","params":{"item":{"childSessionId":"6b57a3c2-dcbd-490e-a2ed-56107a04c4ea","fallbackText":"Reminder child session","generationId":10,"itemId":"a757ea2c-e547-432b-a470-b71f7c83dcf4","kind":"reminderChild","recordedAt":"2026-09-29T17:51:50.955406Z","reminderAgentId":"skill-reminder","revision":1,"status":"inProgress","taskId":"01a0ee4b-5aaa-7ac1-a9bc-425cfedf8c15","turnId":"01a0ee49-ffce-759b-a6f5-0a15a6623fa1"},"sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","sourceRange":{"first":{"id":"a757ea2c-e547-432b-a470-b71f7c83dcf4","sequence":431},"last":{"id":"a757ea2c-e547-432b-a470-b71f7c83dcf4","sequence":431},"stream":{"id":"01a0ee49-ff75-70e2-83fc-3169de40b80c","kind":"session"}},"viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:42"}}} +{"dir":"in","msg":{"emittedAtMs":1790704313774,"jsonrpc":"2.0","method":"item/started","params":{"item":{"itemId":"ee2ae138-5529-4ff3-b271-6f92065dcf0d","kind":"agentMessage","revision":1,"status":"inProgress","text":"","turnId":"01a0ee49-ffce-759b-a6f5-0a15a6623fa1"},"sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:42.1"}}} +{"dir":"in","msg":{"emittedAtMs":1790704313774,"jsonrpc":"2.0","method":"item/delta","params":{"delta":"I could not retrieve the marker.\n\n","field":"text","itemId":"ee2ae138-5529-4ff3-b271-6f92065dcf0d","sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:42.2"}}} +{"dir":"in","msg":{"emittedAtMs":1790704313774,"jsonrpc":"2.0","method":"item/delta","params":{"delta":"I attempted to call `mcp__verification__verification_echo`","field":"text","itemId":"ee2ae138-5529-4ff3-b271-6f92065dcf0d","sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:42.3"}}} +{"dir":"in","msg":{"emittedAtMs":1790704313774,"jsonrpc":"2.0","method":"item/delta","params":{"delta":" with empty arguments","field":"text","itemId":"ee2ae138-5529-4ff3-b271-6f92065dcf0d","sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:42.4"}}} +{"dir":"in","msg":{"emittedAtMs":1790704313774,"jsonrpc":"2.0","method":"item/delta","params":{"delta":" `{","field":"text","itemId":"ee2ae138-5529-4ff3-b271-6f92065dcf0d","sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:42.5"}}} +{"dir":"in","msg":{"emittedAtMs":1790704314078,"jsonrpc":"2.0","method":"item/delta","params":{"delta":"}` multiple times, but each attempt","field":"text","itemId":"ee2ae138-5529-4ff3-b271-6f92065dcf0d","sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:42.6"}}} +{"dir":"in","msg":{"emittedAtMs":1790704314078,"jsonrpc":"2.0","method":"item/delta","params":{"delta":" was routed to the","field":"text","itemId":"ee2ae138-5529-4ff3-b271-6f92065dcf0d","sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:42.7"}}} +{"dir":"in","msg":{"emittedAtMs":1790704314079,"jsonrpc":"2.0","method":"item/delta","params":{"delta":" wrong tool identifier","field":"text","itemId":"ee2ae138-5529-4ff3-b271-6f92065dcf0d","sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:42.8"}}} +{"dir":"in","msg":{"emittedAtMs":1790704314079,"jsonrpc":"2.0","method":"item/delta","params":{"delta":" (`muse.verification_echo`","field":"text","itemId":"ee2ae138-5529-4ff3-b271-6f92065dcf0d","sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:42.9"}}} +{"dir":"in","msg":{"emittedAtMs":1790704314079,"jsonrpc":"2.0","method":"item/delta","params":{"delta":" instead of `default.mcp__verification__verification_echo`)","field":"text","itemId":"ee2ae138-5529-4ff3-b271-6f92065dcf0d","sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:42.10"}}} +{"dir":"in","msg":{"emittedAtMs":1790704314386,"jsonrpc":"2.0","method":"item/delta","params":{"delta":" and returned `","field":"text","itemId":"ee2ae138-5529-4ff3-b271-6f92065dcf0d","sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:42.11"}}} +{"dir":"in","msg":{"emittedAtMs":1790704314386,"jsonrpc":"2.0","method":"item/delta","params":{"delta":"tool unavailable: unknown tool`.","field":"text","itemId":"ee2ae138-5529-4ff3-b271-6f92065dcf0d","sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:42.12"}}} +{"dir":"in","msg":{"emittedAtMs":1790704314386,"jsonrpc":"2.0","method":"item/delta","params":{"delta":" I","field":"text","itemId":"ee2ae138-5529-4ff3-b271-6f92065dcf0d","sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:42.13"}}} +{"dir":"in","msg":{"emittedAtMs":1790704314386,"jsonrpc":"2.0","method":"item/delta","params":{"delta":" was","field":"text","itemId":"ee2ae138-5529-4ff3-b271-6f92065dcf0d","sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:42.14"}}} +{"dir":"in","msg":{"emittedAtMs":1790704314397,"jsonrpc":"2.0","method":"item/delta","params":{"delta":" unable to","field":"text","itemId":"ee2ae138-5529-4ff3-b271-6f92065dcf0d","sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:42.15"}}} +{"dir":"in","msg":{"emittedAtMs":1790704314456,"jsonrpc":"2.0","method":"item/delta","params":{"delta":" correct","field":"text","itemId":"ee2ae138-5529-4ff3-b271-6f92065dcf0d","sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:42.16"}}} +{"dir":"in","msg":{"emittedAtMs":1790704314515,"jsonrpc":"2.0","method":"item/delta","params":{"delta":" the routing,","field":"text","itemId":"ee2ae138-5529-4ff3-b271-6f92065dcf0d","sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:42.17"}}} +{"dir":"in","msg":{"emittedAtMs":1790704314577,"jsonrpc":"2.0","method":"item/delta","params":{"delta":" so no marker was ever","field":"text","itemId":"ee2ae138-5529-4ff3-b271-6f92065dcf0d","sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:42.18"}}} +{"dir":"in","msg":{"emittedAtMs":1790704314642,"jsonrpc":"2.0","method":"item/delta","params":{"delta":" returned.\n\nNo marker to","field":"text","itemId":"ee2ae138-5529-4ff3-b271-6f92065dcf0d","sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:42.19"}}} +{"dir":"in","msg":{"emittedAtMs":1790704314706,"jsonrpc":"2.0","method":"item/delta","params":{"delta":" report","field":"text","itemId":"ee2ae138-5529-4ff3-b271-6f92065dcf0d","sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:42.20"}}} +{"dir":"in","msg":{"emittedAtMs":1790704314770,"jsonrpc":"2.0","method":"item/delta","params":{"delta":".","field":"text","itemId":"ee2ae138-5529-4ff3-b271-6f92065dcf0d","sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:42.21"}}} +{"dir":"in","msg":{"emittedAtMs":1790704314867,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"itemId":"ee2ae138-5529-4ff3-b271-6f92065dcf0d","kind":"agentMessage","recordedAt":"2026-09-29T17:51:54.866963Z","revision":2,"status":"completed","text":"I could not retrieve the marker.\n\nI attempted to call `mcp__verification__verification_echo` with empty arguments `{}` multiple times, but each attempt was routed to the wrong tool identifier (`muse.verification_echo` instead of `default.mcp__verification__verification_echo`) and returned `tool unavailable: unknown tool`. I was unable to correct the routing, so no marker was ever returned.\n\nNo marker to report.","turnId":"01a0ee49-ffce-759b-a6f5-0a15a6623fa1"},"sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","sourceRange":{"first":{"id":"add1291a-31e0-4411-98be-80a35374254f","sequence":444},"last":{"id":"add1291a-31e0-4411-98be-80a35374254f","sequence":444},"stream":{"id":"01a0ee49-ff75-70e2-83fc-3169de40b80c","kind":"session"}},"viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:43"}}} +{"dir":"in","msg":{"emittedAtMs":1790704314911,"jsonrpc":"2.0","method":"session/tokenUsage","params":{"cumulative":{"outputTokens":4299,"promptTokens":494183,"totalTokens":498482},"durationMs":3951,"modelId":"meta/muse-spark-1.3","promptTokens":31328,"sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","sourceRange":{"first":{"id":"0423908e-828a-45cf-95f8-0fb29d65f6b6","sequence":447},"last":{"id":"0423908e-828a-45cf-95f8-0fb29d65f6b6","sequence":447},"stream":{"id":"01a0ee49-ff75-70e2-83fc-3169de40b80c","kind":"session"}},"totalTokens":31497,"turnId":"01a0ee49-ffce-759b-a6f5-0a15a6623fa1","usage":{"cacheReadTokens":8305,"cacheWriteTokens":0,"cachedTokens":8305,"inputTokens":31328,"outputTokens":169,"reasoningTokens":73},"viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:44"}}} +{"dir":"in","msg":{"emittedAtMs":1790704314914,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"childSessionId":"6b57a3c2-dcbd-490e-a2ed-56107a04c4ea","failureReason":"main run completed","fallbackText":"Reminder child session","generationId":10,"itemId":"a757ea2c-e547-432b-a470-b71f7c83dcf4","kind":"reminderChild","recordedAt":"2026-09-29T17:51:54.91425Z","reminderAgentId":"skill-reminder","revision":2,"status":"cancelled","taskId":"01a0ee4b-5aaa-7ac1-a9bc-425cfedf8c15","turnId":"01a0ee49-ffce-759b-a6f5-0a15a6623fa1"},"sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","sourceRange":{"first":{"id":"a757ea2c-e547-432b-a470-b71f7c83dcf4","sequence":431},"last":{"id":"1cf76c07-17ff-4fef-9fdd-8324d008a71b","sequence":450},"stream":{"id":"01a0ee49-ff75-70e2-83fc-3169de40b80c","kind":"session"}},"viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:45"}}} +{"dir":"in","msg":{"emittedAtMs":1790704314914,"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","status":"idle","viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:46"}}} +{"dir":"in","msg":{"emittedAtMs":1790704314915,"jsonrpc":"2.0","method":"turn/completed","params":{"durationMs":92752,"sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","sourceRange":{"first":{"id":"5bf0ae8a-ac86-437d-acf4-9e4cae7ca219","sequence":452},"last":{"id":"5bf0ae8a-ac86-437d-acf4-9e4cae7ca219","sequence":452},"stream":{"id":"01a0ee49-ff75-70e2-83fc-3169de40b80c","kind":"session"}},"terminal":"completed","timeToFirstTokenMs":28541,"turnId":"01a0ee49-ffce-759b-a6f5-0a15a6623fa1","viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:46"}}} +{"dir":"in","msg":{"emittedAtMs":1790704314916,"jsonrpc":"2.0","method":"session/listChanged","params":{"session":{"activeTurnId":null,"branch":"main","createdAt":"2026-09-29T17:50:22.069281Z","firstUserPrompt":"Call the tool mcp__verification__verification_echo with empty arguments {}, then reply with the marker it returns. This is a tool invocation test, do not use skills, shell, or subagents.","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee49-ff75-70e2-83fc-3169de40b80c/session.jsonl","providerId":"meta","sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","status":"idle","title":"Call the tool mcp__verification__verification_echo with empty arguments {}, then reply with the marker it returns. This is a tool invocation test, do not use skills, shell, or subagents.","turnCount":1,"updatedAt":"2026-09-29T17:51:54.914491Z","workspaceRoot":"/tmp/muse-phase0/repo"}}}} +{"dir":"in","msg":{"emittedAtMs":1790704315172,"jsonrpc":"2.0","method":"session/closed","params":{"reason":"hostShutdown","sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:47"}}} +{"dir":"in","msg":{"emittedAtMs":1790704315172,"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","status":"notLoaded","viewCursor":"v:01a0ee49-ff75-70e2-83fc-3169de40b80c:47"}}} +{"dir":"in","msg":{"emittedAtMs":1790704315172,"jsonrpc":"2.0","method":"session/listChanged","params":{"session":{"activeTurnId":null,"branch":"main","createdAt":"2026-09-29T17:50:22.069281Z","firstUserPrompt":"Call the tool mcp__verification__verification_echo with empty arguments {}, then reply with the marker it returns. This is a tool invocation test, do not use skills, shell, or subagents.","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee49-ff75-70e2-83fc-3169de40b80c/session.jsonl","providerId":"meta","sessionId":"01a0ee49-ff75-70e2-83fc-3169de40b80c","status":"notLoaded","title":"Call the tool mcp__verification__verification_echo with empty arguments {}, then reply with the marker it returns. This is a tool invocation test, do not use skills, shell, or subagents.","turnCount":1,"updatedAt":"2026-09-29T17:51:54.916673Z","workspaceRoot":"/tmp/muse-phase0/repo"}}}} diff --git a/plugins/muse-provider/test/fixtures/multi-stage-approval.ndjson b/plugins/muse-provider/test/fixtures/multi-stage-approval.ndjson new file mode 100644 index 0000000000..ddf84005c6 --- /dev/null +++ b/plugins/muse-provider/test/fixtures/multi-stage-approval.ndjson @@ -0,0 +1,51 @@ +{"dir":"out","msg":{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"clientInfo":{"name":"paseo_phase0","version":"0.0.0"},"capabilities":{"experimentalApi":true,"requestedCapabilities":["sessionMcp","sessionListStream"]}}}} +{"dir":"in","msg":{"id":1,"jsonrpc":"2.0","result":{"experimentalApi":true,"grantedCapabilities":["sessionMcp","sessionListStream"],"museHome":"/tmp/muse-phase0/data/muse","platformFamily":"unix","platformOs":"linux","schema":{"fingerprint":"sha256:e0e163db6ccf00dbe68402ce55d6319b3edc33c421f31e9583b587b2de8a118f","version":1},"serverInfo":{"name":"muse","version":"1.4.1"},"sessionDurability":"durable","userAgent":"muse-build/1.4.1 (non-interactive; linux-x86_64; build 35815c253477406321e3f5595195becc25e3907a)"}}} +{"dir":"out","msg":{"jsonrpc":"2.0","method":"initialized","params":{}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":2,"method":"session/start","params":{"commandId":"01a0ee4b-2759-73fd-a871-0d2558d2d5ba","workspaceRoot":"/tmp/muse-phase0/repo","modelId":"meta/muse-spark-1.3","providerId":"meta","approvalMode":"onRequest"}}} +{"dir":"in","msg":{"emittedAtMs":1790704297906,"jsonrpc":"2.0","method":"session/started","params":{"session":{"activeTurnId":null,"approvalMode":{"lastCommandId":null,"mode":"onRequest","source":"startup"},"createdAt":"2026-09-29T17:51:37.827408Z","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee4b-2760-76c3-823d-1923d8fa3eff/session.jsonl","providerId":"meta","sessionId":"01a0ee4b-2760-76c3-823d-1923d8fa3eff","status":"idle","turnCount":0,"updatedAt":"2026-09-29T17:51:37.827412Z","workspaceRoot":"/tmp/muse-phase0/repo"}}}} +{"dir":"in","msg":{"id":2,"jsonrpc":"2.0","result":{"session":{"activeTurnId":null,"approvalMode":{"lastCommandId":null,"mode":"onRequest","source":"startup"},"createdAt":"2026-09-29T17:51:37.827408Z","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee4b-2760-76c3-823d-1923d8fa3eff/session.jsonl","providerId":"meta","sessionId":"01a0ee4b-2760-76c3-823d-1923d8fa3eff","status":"idle","turnCount":0,"updatedAt":"2026-09-29T17:51:37.827412Z","workspaceRoot":"/tmp/muse-phase0/repo"},"viewCursor":"v:01a0ee4b-2760-76c3-823d-1923d8fa3eff:1"}}} +{"dir":"in","msg":{"emittedAtMs":1790704297912,"jsonrpc":"2.0","method":"session/approvalModeChanged","params":{"clientName":"paseo_phase0","commandId":"01a0ee4b-2759-73fd-a871-0d2558d2d5ba","mode":"onRequest","sessionId":"01a0ee4b-2760-76c3-823d-1923d8fa3eff","source":"approvalReconfigure","sourceRange":{"first":{"id":"460e980f-c22f-4173-945c-5b4e7aaf6125","sequence":5},"last":{"id":"460e980f-c22f-4173-945c-5b4e7aaf6125","sequence":5},"stream":{"id":"01a0ee4b-2760-76c3-823d-1923d8fa3eff","kind":"session"}},"viewCursor":"v:01a0ee4b-2760-76c3-823d-1923d8fa3eff:1"}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":3,"method":"turn/start","params":{"commandId":"01a0ee4b-27b8-7039-abed-754eaf12122c","sessionId":"01a0ee4b-2760-76c3-823d-1923d8fa3eff","input":[{"type":"text","text":"Run exactly wc -l f && rm -rf f && rm -rf g using the shell tool. f and g are disposable test data and I authorize deleting both. Do not split the command into separate calls."}],"reasoningEffort":"low"}}} +{"dir":"in","msg":{"emittedAtMs":1790704297915,"jsonrpc":"2.0","method":"session/branchChanged","params":{"branch":"main","sessionId":"01a0ee4b-2760-76c3-823d-1923d8fa3eff","sourceRange":{"first":{"id":"c8098940-10b7-466e-8314-07a94c85831b","sequence":15},"last":{"id":"c8098940-10b7-466e-8314-07a94c85831b","sequence":15},"stream":{"id":"01a0ee4b-2760-76c3-823d-1923d8fa3eff","kind":"session"}},"vcs":"git","viewCursor":"v:01a0ee4b-2760-76c3-823d-1923d8fa3eff:2","workspaceRoot":"/tmp/muse-phase0/repo"}}} +{"dir":"in","msg":{"id":3,"jsonrpc":"2.0","result":{"commandId":"01a0ee4b-27b8-7039-abed-754eaf12122c","disposition":"started","startedNewTurn":true,"status":"accepted","turnId":"01a0ee4b-27b8-7039-abed-754eaf12122c"}}} +{"dir":"in","msg":{"emittedAtMs":1790704297924,"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a0ee4b-2760-76c3-823d-1923d8fa3eff","status":"running","viewCursor":"v:01a0ee4b-2760-76c3-823d-1923d8fa3eff:3"}}} +{"dir":"in","msg":{"emittedAtMs":1790704297925,"jsonrpc":"2.0","method":"turn/started","params":{"commandId":"01a0ee4b-27b8-7039-abed-754eaf12122c","sessionId":"01a0ee4b-2760-76c3-823d-1923d8fa3eff","sourceRange":{"first":{"id":"4c24b42f-4ccd-41e6-90b8-6c6940fde511","sequence":17},"last":{"id":"4c24b42f-4ccd-41e6-90b8-6c6940fde511","sequence":17},"stream":{"id":"01a0ee4b-2760-76c3-823d-1923d8fa3eff","kind":"session"}},"turnId":"01a0ee4b-27b8-7039-abed-754eaf12122c","viewCursor":"v:01a0ee4b-2760-76c3-823d-1923d8fa3eff:3"}}} +{"dir":"in","msg":{"emittedAtMs":1790704297925,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"commandId":"01a0ee4b-27b8-7039-abed-754eaf12122c","itemId":"f654f052-0f13-4fcd-844d-a454f8dd7299","kind":"userMessage","recordedAt":"2026-09-29T17:51:37.916056Z","revision":1,"status":"completed","text":"Run exactly wc -l f && rm -rf f && rm -rf g using the shell tool. f and g are disposable test data and I authorize deleting both. Do not split the command into separate calls.","turnId":"01a0ee4b-27b8-7039-abed-754eaf12122c"},"sessionId":"01a0ee4b-2760-76c3-823d-1923d8fa3eff","sourceRange":{"first":{"id":"4c24b42f-4ccd-41e6-90b8-6c6940fde511","sequence":17},"last":{"id":"4c24b42f-4ccd-41e6-90b8-6c6940fde511","sequence":17},"stream":{"id":"01a0ee4b-2760-76c3-823d-1923d8fa3eff","kind":"session"}},"viewCursor":"v:01a0ee4b-2760-76c3-823d-1923d8fa3eff:4"}}} +{"dir":"in","msg":{"emittedAtMs":1790704297925,"jsonrpc":"2.0","method":"item/started","params":{"item":{"childSessionId":"7d65b0ca-7775-4ff8-b346-7488f99e1672","fallbackText":"Reminder child session","generationId":1,"itemId":"e81fbc29-204f-4ee5-ae6d-d78b936637a3","kind":"reminderChild","recordedAt":"2026-09-29T17:51:37.924053Z","reminderAgentId":"skill-reminder","revision":1,"status":"inProgress","taskId":"01a0ee4b-27c2-70b0-904b-1e139d270837","turnId":"01a0ee4b-27b8-7039-abed-754eaf12122c"},"sessionId":"01a0ee4b-2760-76c3-823d-1923d8fa3eff","sourceRange":{"first":{"id":"e81fbc29-204f-4ee5-ae6d-d78b936637a3","sequence":35},"last":{"id":"e81fbc29-204f-4ee5-ae6d-d78b936637a3","sequence":35},"stream":{"id":"01a0ee4b-2760-76c3-823d-1923d8fa3eff","kind":"session"}},"viewCursor":"v:01a0ee4b-2760-76c3-823d-1923d8fa3eff:5"}}} +{"dir":"in","msg":{"emittedAtMs":1790704297929,"jsonrpc":"2.0","method":"session/listChanged","params":{"session":{"activeTurnId":"01a0ee4b-27b8-7039-abed-754eaf12122c","createdAt":"2026-09-29T17:51:37.827408Z","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee4b-2760-76c3-823d-1923d8fa3eff/session.jsonl","providerId":"meta","sessionId":"01a0ee4b-2760-76c3-823d-1923d8fa3eff","status":"running","turnCount":0,"updatedAt":"2026-09-29T17:51:37.924312Z","workspaceRoot":"/tmp/muse-phase0/repo"}}}} +{"dir":"in","msg":{"emittedAtMs":1790704300174,"jsonrpc":"2.0","method":"session/tokenUsage","params":{"cumulative":{"outputTokens":137,"promptTokens":38164,"totalTokens":38301},"durationMs":2246,"finishReason":"tool_calls","modelId":"meta/muse-spark-1.3","promptTokens":38164,"sessionId":"01a0ee4b-2760-76c3-823d-1923d8fa3eff","sourceRange":{"first":{"id":"1ec4f1ac-a7ad-412e-ac6e-164b82f6684a","sequence":51},"last":{"id":"1ec4f1ac-a7ad-412e-ac6e-164b82f6684a","sequence":51},"stream":{"id":"01a0ee4b-2760-76c3-823d-1923d8fa3eff","kind":"session"}},"totalTokens":38301,"turnId":"01a0ee4b-27b8-7039-abed-754eaf12122c","usage":{"cacheReadTokens":0,"cacheWriteTokens":0,"cachedTokens":0,"inputTokens":38164,"outputTokens":137,"reasoningTokens":36},"viewCursor":"v:01a0ee4b-2760-76c3-823d-1923d8fa3eff:6"}}} +{"dir":"in","msg":{"emittedAtMs":1790704300176,"jsonrpc":"2.0","method":"session/statusChanged","params":{"attention":["approvalPending"],"sessionId":"01a0ee4b-2760-76c3-823d-1923d8fa3eff","status":"running","viewCursor":"v:01a0ee4b-2760-76c3-823d-1923d8fa3eff:7"}}} +{"dir":"in","msg":{"id":1,"jsonrpc":"2.0","method":"approval/request","params":{"approvalId":"01a0ee4b-308f-7722-bb35-4ae0f509511e","availableChoices":[{"choiceId":"allow_once","decision":"approved","label":"Allow once","scope":"once"},{"choiceId":"allow_local_prefix","decision":"approvedPolicyAmendment","label":"Always allow in this workspace: rm ...","rulePreview":"Always allow in this workspace: rm ...","scope":"localPersistent"},{"acceptsFeedback":true,"choiceId":"abort","decision":"abort","label":"Reject","scope":"once"}],"currentRequirementId":{"approvalId":"01a0ee4b-308f-7722-bb35-4ae0f509511e","sourceIndex":1},"itemId":"01a0ee4b-308f-7722-bb35-4ae0f509511e","judgeEscalated":false,"protectedWrite":false,"rawArgs":"{\"command\":\"wc -l f && rm -rf f && rm -rf g\",\"description\":\"Run wc and cleanup test files\"}","sessionId":"01a0ee4b-2760-76c3-823d-1923d8fa3eff","sourceRange":{"first":{"id":"59dd6969-a7ba-404e-b8ec-e229e81ecc7c","sequence":56},"last":{"id":"59dd6969-a7ba-404e-b8ec-e229e81ecc7c","sequence":56},"stream":{"id":"01a0ee4b-2760-76c3-823d-1923d8fa3eff","kind":"session"}},"subject":{"command":"wc -l f && rm -rf f && rm -rf g","kind":"shell","stages":[{"argv":["wc","-l","f"],"argvComplete":true,"position":1,"requirementId":{"approvalId":"01a0ee4b-308f-7722-bb35-4ae0f509511e","sourceIndex":0},"resolution":{"kind":"knownSafe"},"suggestedPrefix":{"argvPrefix":["wc"],"label":"Always allow in this workspace: wc ..."},"totalStages":3},{"argv":["rm","-rf","f"],"argvComplete":true,"position":2,"requirementId":{"approvalId":"01a0ee4b-308f-7722-bb35-4ae0f509511e","sourceIndex":1},"resolution":{"kind":"unresolved"},"suggestedPrefix":{"argvPrefix":["rm"],"label":"Always allow in this workspace: rm ..."},"totalStages":3},{"argv":["rm","-rf","g"],"argvComplete":true,"position":3,"requirementId":{"approvalId":"01a0ee4b-308f-7722-bb35-4ae0f509511e","sourceIndex":2},"resolution":{"kind":"unresolved"},"suggestedPrefix":{"argvPrefix":["rm"],"label":"Always allow in this workspace: rm ..."},"totalStages":3}],"workspaceRoot":"/tmp/muse-phase0/repo"},"taskId":"01a0ee4b-308f-7722-bb35-4ae0f509511e","toolCallId":"call_01a0ee4b2e44774096a3a731e986cd1f","toolName":"bash","turnId":"01a0ee4b-27b8-7039-abed-754eaf12122c","viewCursor":"v:01a0ee4b-2760-76c3-823d-1923d8fa3eff:7"}}} +{"dir":"in","msg":{"emittedAtMs":1790704300178,"jsonrpc":"2.0","method":"approval/requested","params":{"approvalId":"01a0ee4b-308f-7722-bb35-4ae0f509511e","availableChoices":[{"choiceId":"allow_once","decision":"approved","label":"Allow once","scope":"once"},{"choiceId":"allow_local_prefix","decision":"approvedPolicyAmendment","label":"Always allow in this workspace: rm ...","rulePreview":"Always allow in this workspace: rm ...","scope":"localPersistent"},{"acceptsFeedback":true,"choiceId":"abort","decision":"abort","label":"Reject","scope":"once"}],"currentRequirementId":{"approvalId":"01a0ee4b-308f-7722-bb35-4ae0f509511e","sourceIndex":1},"itemId":"01a0ee4b-308f-7722-bb35-4ae0f509511e","judgeEscalated":false,"protectedWrite":false,"rawArgs":"{\"command\":\"wc -l f && rm -rf f && rm -rf g\",\"description\":\"Run wc and cleanup test files\"}","sessionId":"01a0ee4b-2760-76c3-823d-1923d8fa3eff","sourceRange":{"first":{"id":"59dd6969-a7ba-404e-b8ec-e229e81ecc7c","sequence":56},"last":{"id":"59dd6969-a7ba-404e-b8ec-e229e81ecc7c","sequence":56},"stream":{"id":"01a0ee4b-2760-76c3-823d-1923d8fa3eff","kind":"session"}},"subject":{"command":"wc -l f && rm -rf f && rm -rf g","kind":"shell","stages":[{"argv":["wc","-l","f"],"argvComplete":true,"position":1,"requirementId":{"approvalId":"01a0ee4b-308f-7722-bb35-4ae0f509511e","sourceIndex":0},"resolution":{"kind":"knownSafe"},"suggestedPrefix":{"argvPrefix":["wc"],"label":"Always allow in this workspace: wc ..."},"totalStages":3},{"argv":["rm","-rf","f"],"argvComplete":true,"position":2,"requirementId":{"approvalId":"01a0ee4b-308f-7722-bb35-4ae0f509511e","sourceIndex":1},"resolution":{"kind":"unresolved"},"suggestedPrefix":{"argvPrefix":["rm"],"label":"Always allow in this workspace: rm ..."},"totalStages":3},{"argv":["rm","-rf","g"],"argvComplete":true,"position":3,"requirementId":{"approvalId":"01a0ee4b-308f-7722-bb35-4ae0f509511e","sourceIndex":2},"resolution":{"kind":"unresolved"},"suggestedPrefix":{"argvPrefix":["rm"],"label":"Always allow in this workspace: rm ..."},"totalStages":3}],"workspaceRoot":"/tmp/muse-phase0/repo"},"taskId":"01a0ee4b-308f-7722-bb35-4ae0f509511e","toolCallId":"call_01a0ee4b2e44774096a3a731e986cd1f","toolName":"bash","turnId":"01a0ee4b-27b8-7039-abed-754eaf12122c","viewCursor":"v:01a0ee4b-2760-76c3-823d-1923d8fa3eff:7"}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":1,"result":{}}} +{"dir":"in","msg":{"emittedAtMs":1790704300178,"jsonrpc":"2.0","method":"session/listChanged","params":{"session":{"activeTurnId":"01a0ee4b-27b8-7039-abed-754eaf12122c","attention":["approvalPending"],"branch":"main","createdAt":"2026-09-29T17:51:37.824904Z","firstUserPrompt":"Run exactly wc -l f && rm -rf f && rm -rf g using the shell tool. f and g are disposable test data and I authorize deleting both. Do not split the command into separate calls.","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee4b-2760-76c3-823d-1923d8fa3eff/session.jsonl","providerId":"meta","sessionId":"01a0ee4b-2760-76c3-823d-1923d8fa3eff","status":"running","title":"Run exactly wc -l f && rm -rf f && rm -rf g using the shell tool. f and g are disposable test data and I authorize deleting both. Do not split the command into separate calls.","turnCount":0,"updatedAt":"2026-09-29T17:51:40.176687Z","workspaceRoot":"/tmp/muse-phase0/repo"}}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":4,"method":"approval/decide","params":{"commandId":"01a0ee4b-3092-742d-a166-2f6f2bdceef9","sessionId":"01a0ee4b-2760-76c3-823d-1923d8fa3eff","approvalId":"01a0ee4b-308f-7722-bb35-4ae0f509511e","requirementId":{"approvalId":"01a0ee4b-308f-7722-bb35-4ae0f509511e","sourceIndex":1},"choiceId":"allow_once"}}} +{"dir":"in","msg":{"id":4,"jsonrpc":"2.0","result":{"approvalId":"01a0ee4b-308f-7722-bb35-4ae0f509511e","commandId":"01a0ee4b-3092-742d-a166-2f6f2bdceef9","status":"accepted","terminal":false}}} +{"dir":"in","msg":{"emittedAtMs":1790704300188,"jsonrpc":"2.0","method":"approval/updated","params":{"approvalId":"01a0ee4b-308f-7722-bb35-4ae0f509511e","availableChoices":[{"choiceId":"allow_once","decision":"approved","label":"Allow once","scope":"once"},{"choiceId":"allow_local_prefix","decision":"approvedPolicyAmendment","label":"Always allow in this workspace: rm ...","rulePreview":"Always allow in this workspace: rm ...","scope":"localPersistent"},{"acceptsFeedback":true,"choiceId":"abort","decision":"abort","label":"Reject","scope":"once"}],"change":{"choiceId":"allow_once","decision":"approved","kind":"stageResolved","requirementId":{"approvalId":"01a0ee4b-308f-7722-bb35-4ae0f509511e","sourceIndex":1}},"currentRequirementId":{"approvalId":"01a0ee4b-308f-7722-bb35-4ae0f509511e","sourceIndex":2},"sessionId":"01a0ee4b-2760-76c3-823d-1923d8fa3eff","sourceRange":{"first":{"id":"3b8c6655-54b5-4389-a77e-d8cefe86308e","sequence":59},"last":{"id":"3b8c6655-54b5-4389-a77e-d8cefe86308e","sequence":59},"stream":{"id":"01a0ee4b-2760-76c3-823d-1923d8fa3eff","kind":"session"}},"subject":{"command":"wc -l f && rm -rf f && rm -rf g","kind":"shell","stages":[{"argv":["wc","-l","f"],"argvComplete":true,"position":1,"requirementId":{"approvalId":"01a0ee4b-308f-7722-bb35-4ae0f509511e","sourceIndex":0},"resolution":{"kind":"knownSafe"},"suggestedPrefix":{"argvPrefix":["wc"],"label":"Always allow in this workspace: wc ..."},"totalStages":3},{"argv":["rm","-rf","f"],"argvComplete":true,"position":2,"requirementId":{"approvalId":"01a0ee4b-308f-7722-bb35-4ae0f509511e","sourceIndex":1},"resolution":{"kind":"allowOnce"},"suggestedPrefix":{"argvPrefix":["rm"],"label":"Always allow in this workspace: rm ..."},"totalStages":3},{"argv":["rm","-rf","g"],"argvComplete":true,"position":3,"requirementId":{"approvalId":"01a0ee4b-308f-7722-bb35-4ae0f509511e","sourceIndex":2},"resolution":{"kind":"unresolved"},"suggestedPrefix":{"argvPrefix":["rm"],"label":"Always allow in this workspace: rm ..."},"totalStages":3}],"workspaceRoot":"/tmp/muse-phase0/repo"},"viewCursor":"v:01a0ee4b-2760-76c3-823d-1923d8fa3eff:8"}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":5,"method":"approval/decide","params":{"commandId":"01a0ee4b-309c-765c-a4a7-3fbbe41fe0a7","sessionId":"01a0ee4b-2760-76c3-823d-1923d8fa3eff","approvalId":"01a0ee4b-308f-7722-bb35-4ae0f509511e","requirementId":{"approvalId":"01a0ee4b-308f-7722-bb35-4ae0f509511e","sourceIndex":2},"choiceId":"allow_once"}}} +{"dir":"in","msg":{"id":5,"jsonrpc":"2.0","result":{"approvalId":"01a0ee4b-308f-7722-bb35-4ae0f509511e","commandId":"01a0ee4b-309c-765c-a4a7-3fbbe41fe0a7","status":"accepted","terminal":true}}} +{"dir":"in","msg":{"emittedAtMs":1790704300196,"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a0ee4b-2760-76c3-823d-1923d8fa3eff","status":"running","viewCursor":"v:01a0ee4b-2760-76c3-823d-1923d8fa3eff:10"}}} +{"dir":"in","msg":{"emittedAtMs":1790704300196,"jsonrpc":"2.0","method":"approval/updated","params":{"approvalId":"01a0ee4b-308f-7722-bb35-4ae0f509511e","availableChoices":[{"choiceId":"allow_once","decision":"approved","label":"Allow once","scope":"once"},{"choiceId":"allow_local_prefix","decision":"approvedPolicyAmendment","label":"Always allow in this workspace: rm ...","rulePreview":"Always allow in this workspace: rm ...","scope":"localPersistent"},{"acceptsFeedback":true,"choiceId":"abort","decision":"abort","label":"Reject","scope":"once"}],"change":{"choiceId":"allow_once","decision":"approved","kind":"stageResolved","requirementId":{"approvalId":"01a0ee4b-308f-7722-bb35-4ae0f509511e","sourceIndex":2}},"currentRequirementId":{"approvalId":"01a0ee4b-308f-7722-bb35-4ae0f509511e","sourceIndex":2},"sessionId":"01a0ee4b-2760-76c3-823d-1923d8fa3eff","sourceRange":{"first":{"id":"cb3ab79b-cb07-4aa6-9c00-cbcc65718efd","sequence":62},"last":{"id":"cb3ab79b-cb07-4aa6-9c00-cbcc65718efd","sequence":62},"stream":{"id":"01a0ee4b-2760-76c3-823d-1923d8fa3eff","kind":"session"}},"subject":{"command":"wc -l f && rm -rf f && rm -rf g","kind":"shell","stages":[{"argv":["wc","-l","f"],"argvComplete":true,"position":1,"requirementId":{"approvalId":"01a0ee4b-308f-7722-bb35-4ae0f509511e","sourceIndex":0},"resolution":{"kind":"knownSafe"},"suggestedPrefix":{"argvPrefix":["wc"],"label":"Always allow in this workspace: wc ..."},"totalStages":3},{"argv":["rm","-rf","f"],"argvComplete":true,"position":2,"requirementId":{"approvalId":"01a0ee4b-308f-7722-bb35-4ae0f509511e","sourceIndex":1},"resolution":{"kind":"allowOnce"},"suggestedPrefix":{"argvPrefix":["rm"],"label":"Always allow in this workspace: rm ..."},"totalStages":3},{"argv":["rm","-rf","g"],"argvComplete":true,"position":3,"requirementId":{"approvalId":"01a0ee4b-308f-7722-bb35-4ae0f509511e","sourceIndex":2},"resolution":{"kind":"allowOnce"},"suggestedPrefix":{"argvPrefix":["rm"],"label":"Always allow in this workspace: rm ..."},"totalStages":3}],"workspaceRoot":"/tmp/muse-phase0/repo"},"viewCursor":"v:01a0ee4b-2760-76c3-823d-1923d8fa3eff:9"}}} +{"dir":"in","msg":{"emittedAtMs":1790704300196,"jsonrpc":"2.0","method":"approval/resolved","params":{"approvalId":"01a0ee4b-308f-7722-bb35-4ae0f509511e","decidedByCommandId":"01a0ee4b-309c-765c-a4a7-3fbbe41fe0a7","decision":"approved","itemId":"01a0ee4b-308f-7722-bb35-4ae0f509511e","policyResult":"allow","resolvedBy":"user","sessionId":"01a0ee4b-2760-76c3-823d-1923d8fa3eff","sourceRange":{"first":{"id":"57af2582-82bd-4ab6-b653-c70027b2ba51","sequence":63},"last":{"id":"57af2582-82bd-4ab6-b653-c70027b2ba51","sequence":63},"stream":{"id":"01a0ee4b-2760-76c3-823d-1923d8fa3eff","kind":"session"}},"stageEvidence":[{"argv":["wc","-l","f"],"position":1,"requirementId":{"approvalId":"01a0ee4b-308f-7722-bb35-4ae0f509511e","sourceIndex":0},"resolution":{"kind":"knownSafe"},"totalStages":3},{"argv":["rm","-rf","f"],"position":2,"requirementId":{"approvalId":"01a0ee4b-308f-7722-bb35-4ae0f509511e","sourceIndex":1},"resolution":{"kind":"allowOnce"},"totalStages":3},{"argv":["rm","-rf","g"],"position":3,"requirementId":{"approvalId":"01a0ee4b-308f-7722-bb35-4ae0f509511e","sourceIndex":2},"resolution":{"kind":"allowOnce"},"totalStages":3}],"turnId":"01a0ee4b-27b8-7039-abed-754eaf12122c","viewCursor":"v:01a0ee4b-2760-76c3-823d-1923d8fa3eff:10"}}} +{"dir":"in","msg":{"emittedAtMs":1790704300196,"jsonrpc":"2.0","method":"item/started","params":{"item":{"args":"{\"command\":\"wc -l f && rm -rf f && rm -rf g\",\"description\":\"Run wc and cleanup test files\"}","callId":"call_01a0ee4b2e44774096a3a731e986cd1f","itemId":"01a0ee4b-308f-7722-bb35-4ae0f509511e","kind":"toolCall","recordedAt":"2026-09-29T17:51:40.192737Z","revision":1,"status":"inProgress","tool":"bash","turnId":"01a0ee4b-27b8-7039-abed-754eaf12122c"},"sessionId":"01a0ee4b-2760-76c3-823d-1923d8fa3eff","sourceRange":{"first":{"id":"7128a063-cea5-44c4-a53b-7e53758c4747","sequence":53},"last":{"id":"e8d3acc2-2993-4884-98f1-89ab7cb8a4cc","sequence":69},"stream":{"id":"01a0ee4b-2760-76c3-823d-1923d8fa3eff","kind":"session"}},"viewCursor":"v:01a0ee4b-2760-76c3-823d-1923d8fa3eff:11"}}} +{"dir":"in","msg":{"emittedAtMs":1790704300196,"jsonrpc":"2.0","method":"session/listChanged","params":{"session":{"activeTurnId":"01a0ee4b-27b8-7039-abed-754eaf12122c","branch":"main","createdAt":"2026-09-29T17:51:37.824904Z","firstUserPrompt":"Run exactly wc -l f && rm -rf f && rm -rf g using the shell tool. f and g are disposable test data and I authorize deleting both. Do not split the command into separate calls.","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee4b-2760-76c3-823d-1923d8fa3eff/session.jsonl","providerId":"meta","sessionId":"01a0ee4b-2760-76c3-823d-1923d8fa3eff","status":"running","title":"Run exactly wc -l f && rm -rf f && rm -rf g using the shell tool. f and g are disposable test data and I authorize deleting both. Do not split the command into separate calls.","turnCount":0,"updatedAt":"2026-09-29T17:51:40.193007Z","workspaceRoot":"/tmp/muse-phase0/repo"}}}} +{"dir":"in","msg":{"emittedAtMs":1790704300391,"jsonrpc":"2.0","method":"item/delta","params":{"delta":"2 f\n","field":"output","itemId":"01a0ee4b-308f-7722-bb35-4ae0f509511e","sessionId":"01a0ee4b-2760-76c3-823d-1923d8fa3eff","viewCursor":"v:01a0ee4b-2760-76c3-823d-1923d8fa3eff:11.1"}}} +{"dir":"in","msg":{"emittedAtMs":1790704300397,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"args":"{\"command\":\"wc -l f && rm -rf f && rm -rf g\",\"description\":\"Run wc and cleanup test files\"}","callId":"call_01a0ee4b2e44774096a3a731e986cd1f","itemId":"01a0ee4b-308f-7722-bb35-4ae0f509511e","kind":"toolCall","recordedAt":"2026-09-29T17:51:40.396829Z","revision":2,"status":"completed","tool":"bash","turnId":"01a0ee4b-27b8-7039-abed-754eaf12122c","visibleOutput":"2 f\n"},"sessionId":"01a0ee4b-2760-76c3-823d-1923d8fa3eff","sourceRange":{"first":{"id":"7128a063-cea5-44c4-a53b-7e53758c4747","sequence":53},"last":{"id":"e72253f1-0717-4961-9be9-c0643d9003e8","sequence":75},"stream":{"id":"01a0ee4b-2760-76c3-823d-1923d8fa3eff","kind":"session"}},"viewCursor":"v:01a0ee4b-2760-76c3-823d-1923d8fa3eff:12"}}} +{"dir":"in","msg":{"emittedAtMs":1790704302664,"jsonrpc":"2.0","method":"item/started","params":{"item":{"itemId":"3be55728-b271-422f-a970-e657d9ebc73d","kind":"agentMessage","revision":1,"status":"inProgress","text":"","turnId":"01a0ee4b-27b8-7039-abed-754eaf12122c"},"sessionId":"01a0ee4b-2760-76c3-823d-1923d8fa3eff","viewCursor":"v:01a0ee4b-2760-76c3-823d-1923d8fa3eff:12.1"}}} +{"dir":"in","msg":{"emittedAtMs":1790704302664,"jsonrpc":"2.0","method":"item/delta","params":{"delta":"Ran","field":"text","itemId":"3be55728-b271-422f-a970-e657d9ebc73d","sessionId":"01a0ee4b-2760-76c3-823d-1923d8fa3eff","viewCursor":"v:01a0ee4b-2760-76c3-823d-1923d8fa3eff:12.2"}}} +{"dir":"in","msg":{"emittedAtMs":1790704302664,"jsonrpc":"2.0","method":"item/delta","params":{"delta":" `wc -l f && rm -rf f && rm -rf g`.\n\n","field":"text","itemId":"3be55728-b271-422f-a970-e657d9ebc73d","sessionId":"01a0ee4b-2760-76c3-823d-1923d8fa3eff","viewCursor":"v:01a0ee4b-2760-76c3-823d-1923d8fa3eff:12.3"}}} +{"dir":"in","msg":{"emittedAtMs":1790704302664,"jsonrpc":"2.0","method":"item/delta","params":{"delta":"Output: `2 f`, both","field":"text","itemId":"3be55728-b271-422f-a970-e657d9ebc73d","sessionId":"01a0ee4b-2760-76c3-823d-1923d8fa3eff","viewCursor":"v:01a0ee4b-2760-76c3-823d-1923d8fa3eff:12.4"}}} +{"dir":"in","msg":{"emittedAtMs":1790704302664,"jsonrpc":"2.0","method":"item/delta","params":{"delta":" removed","field":"text","itemId":"3be55728-b271-422f-a970-e657d9ebc73d","sessionId":"01a0ee4b-2760-76c3-823d-1923d8fa3eff","viewCursor":"v:01a0ee4b-2760-76c3-823d-1923d8fa3eff:12.5"}}} +{"dir":"in","msg":{"emittedAtMs":1790704302664,"jsonrpc":"2.0","method":"item/delta","params":{"delta":".","field":"text","itemId":"3be55728-b271-422f-a970-e657d9ebc73d","sessionId":"01a0ee4b-2760-76c3-823d-1923d8fa3eff","viewCursor":"v:01a0ee4b-2760-76c3-823d-1923d8fa3eff:12.6"}}} +{"dir":"in","msg":{"emittedAtMs":1790704302694,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"itemId":"3be55728-b271-422f-a970-e657d9ebc73d","kind":"agentMessage","recordedAt":"2026-09-29T17:51:42.694349Z","revision":2,"status":"completed","text":"Ran `wc -l f && rm -rf f && rm -rf g`.\n\nOutput: `2 f`, both removed.","turnId":"01a0ee4b-27b8-7039-abed-754eaf12122c"},"sessionId":"01a0ee4b-2760-76c3-823d-1923d8fa3eff","sourceRange":{"first":{"id":"24aa0b7d-5900-44f3-ad7b-93a18f40589f","sequence":92},"last":{"id":"24aa0b7d-5900-44f3-ad7b-93a18f40589f","sequence":92},"stream":{"id":"01a0ee4b-2760-76c3-823d-1923d8fa3eff","kind":"session"}},"viewCursor":"v:01a0ee4b-2760-76c3-823d-1923d8fa3eff:13"}}} +{"dir":"in","msg":{"emittedAtMs":1790704302699,"jsonrpc":"2.0","method":"session/tokenUsage","params":{"cumulative":{"outputTokens":177,"promptTokens":76575,"totalTokens":76752},"durationMs":2294,"modelId":"meta/muse-spark-1.3","promptTokens":38411,"sessionId":"01a0ee4b-2760-76c3-823d-1923d8fa3eff","sourceRange":{"first":{"id":"a9b82d76-fa61-4377-bb7c-7b4852fb16fa","sequence":95},"last":{"id":"a9b82d76-fa61-4377-bb7c-7b4852fb16fa","sequence":95},"stream":{"id":"01a0ee4b-2760-76c3-823d-1923d8fa3eff","kind":"session"}},"totalTokens":38451,"turnId":"01a0ee4b-27b8-7039-abed-754eaf12122c","usage":{"cacheReadTokens":38129,"cacheWriteTokens":0,"cachedTokens":38129,"inputTokens":38411,"outputTokens":40,"reasoningTokens":4},"viewCursor":"v:01a0ee4b-2760-76c3-823d-1923d8fa3eff:14"}}} +{"dir":"in","msg":{"emittedAtMs":1790704302701,"jsonrpc":"2.0","method":"item/started","params":{"item":{"childSessionId":"e9cd8200-3ae6-4c05-9b90-8fa93366624e","childSessionLogPath":"subagent/e9cd8200-3ae6-4c05-9b90-8fa93366624e/session.jsonl","fallbackText":"Reminder child session","generationId":1,"itemId":"8c935164-2dd1-4e63-8c77-4000ae7cd25f","kind":"reminderChild","recordedAt":"2026-09-29T17:51:42.700899Z","reminderAgentId":"verify-reminder","revision":1,"status":"inProgress","taskId":"01a0ee4b-3a6b-7150-9b36-5ca39c79d737","turnId":"01a0ee4b-27b8-7039-abed-754eaf12122c"},"sessionId":"01a0ee4b-2760-76c3-823d-1923d8fa3eff","sourceRange":{"first":{"id":"8c935164-2dd1-4e63-8c77-4000ae7cd25f","sequence":102},"last":{"id":"8c935164-2dd1-4e63-8c77-4000ae7cd25f","sequence":102},"stream":{"id":"01a0ee4b-2760-76c3-823d-1923d8fa3eff","kind":"session"}},"viewCursor":"v:01a0ee4b-2760-76c3-823d-1923d8fa3eff:15"}}} +{"dir":"in","msg":{"emittedAtMs":1790704305391,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"childSessionId":"e9cd8200-3ae6-4c05-9b90-8fa93366624e","childSessionLogPath":"subagent/e9cd8200-3ae6-4c05-9b90-8fa93366624e/session.jsonl","fallbackText":"Reminder child session","generationId":1,"itemId":"8c935164-2dd1-4e63-8c77-4000ae7cd25f","kind":"reminderChild","recordedAt":"2026-09-29T17:51:45.390996Z","reminderAgentId":"verify-reminder","revision":2,"status":"completed","taskId":"01a0ee4b-3a6b-7150-9b36-5ca39c79d737","turnId":"01a0ee4b-27b8-7039-abed-754eaf12122c"},"sessionId":"01a0ee4b-2760-76c3-823d-1923d8fa3eff","sourceRange":{"first":{"id":"8c935164-2dd1-4e63-8c77-4000ae7cd25f","sequence":102},"last":{"id":"b3961c2a-59cc-4473-98db-6bfe851ca172","sequence":104},"stream":{"id":"01a0ee4b-2760-76c3-823d-1923d8fa3eff","kind":"session"}},"viewCursor":"v:01a0ee4b-2760-76c3-823d-1923d8fa3eff:16"}}} +{"dir":"in","msg":{"emittedAtMs":1790704305394,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"childSessionId":"7d65b0ca-7775-4ff8-b346-7488f99e1672","failureReason":"main run completed","fallbackText":"Reminder child session","generationId":1,"itemId":"e81fbc29-204f-4ee5-ae6d-d78b936637a3","kind":"reminderChild","recordedAt":"2026-09-29T17:51:45.394425Z","reminderAgentId":"skill-reminder","revision":2,"status":"cancelled","taskId":"01a0ee4b-27c2-70b0-904b-1e139d270837","turnId":"01a0ee4b-27b8-7039-abed-754eaf12122c"},"sessionId":"01a0ee4b-2760-76c3-823d-1923d8fa3eff","sourceRange":{"first":{"id":"e81fbc29-204f-4ee5-ae6d-d78b936637a3","sequence":35},"last":{"id":"f618d1e6-652f-45ac-94ae-c3dd8c76dac4","sequence":108},"stream":{"id":"01a0ee4b-2760-76c3-823d-1923d8fa3eff","kind":"session"}},"viewCursor":"v:01a0ee4b-2760-76c3-823d-1923d8fa3eff:17"}}} +{"dir":"in","msg":{"emittedAtMs":1790704305394,"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a0ee4b-2760-76c3-823d-1923d8fa3eff","status":"idle","viewCursor":"v:01a0ee4b-2760-76c3-823d-1923d8fa3eff:18"}}} +{"dir":"in","msg":{"emittedAtMs":1790704305396,"jsonrpc":"2.0","method":"turn/completed","params":{"durationMs":7478,"sessionId":"01a0ee4b-2760-76c3-823d-1923d8fa3eff","sourceRange":{"first":{"id":"34e5421c-2ac8-46ab-9cee-51687d6949e7","sequence":110},"last":{"id":"34e5421c-2ac8-46ab-9cee-51687d6949e7","sequence":110},"stream":{"id":"01a0ee4b-2760-76c3-823d-1923d8fa3eff","kind":"session"}},"terminal":"completed","timeToFirstTokenMs":4748,"turnId":"01a0ee4b-27b8-7039-abed-754eaf12122c","viewCursor":"v:01a0ee4b-2760-76c3-823d-1923d8fa3eff:18"}}} +{"dir":"in","msg":{"emittedAtMs":1790704305396,"jsonrpc":"2.0","method":"session/branchChanged","params":{"branch":"main","sessionId":"01a0ee4b-2760-76c3-823d-1923d8fa3eff","sourceRange":{"first":{"id":"ce044ab1-0819-437c-a739-35308701504d","sequence":111},"last":{"id":"ce044ab1-0819-437c-a739-35308701504d","sequence":111},"stream":{"id":"01a0ee4b-2760-76c3-823d-1923d8fa3eff","kind":"session"}},"vcs":"git","viewCursor":"v:01a0ee4b-2760-76c3-823d-1923d8fa3eff:19","workspaceRoot":"/tmp/muse-phase0/repo"}}} +{"dir":"in","msg":{"emittedAtMs":1790704305396,"jsonrpc":"2.0","method":"session/listChanged","params":{"session":{"activeTurnId":null,"branch":"main","createdAt":"2026-09-29T17:51:37.824904Z","firstUserPrompt":"Run exactly wc -l f && rm -rf f && rm -rf g using the shell tool. f and g are disposable test data and I authorize deleting both. Do not split the command into separate calls.","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee4b-2760-76c3-823d-1923d8fa3eff/session.jsonl","providerId":"meta","sessionId":"01a0ee4b-2760-76c3-823d-1923d8fa3eff","status":"idle","title":"Run exactly wc -l f && rm -rf f && rm -rf g using the shell tool. f and g are disposable test data and I authorize deleting both. Do not split the command into separate calls.","turnCount":1,"updatedAt":"2026-09-29T17:51:45.395824Z","workspaceRoot":"/tmp/muse-phase0/repo"}}}} +{"dir":"in","msg":{"emittedAtMs":1790704305609,"jsonrpc":"2.0","method":"session/closed","params":{"reason":"hostShutdown","sessionId":"01a0ee4b-2760-76c3-823d-1923d8fa3eff","viewCursor":"v:01a0ee4b-2760-76c3-823d-1923d8fa3eff:19"}}} +{"dir":"in","msg":{"emittedAtMs":1790704305609,"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a0ee4b-2760-76c3-823d-1923d8fa3eff","status":"notLoaded","viewCursor":"v:01a0ee4b-2760-76c3-823d-1923d8fa3eff:19"}}} +{"dir":"in","msg":{"emittedAtMs":1790704305609,"jsonrpc":"2.0","method":"session/listChanged","params":{"session":{"activeTurnId":null,"branch":"main","createdAt":"2026-09-29T17:51:37.824904Z","firstUserPrompt":"Run exactly wc -l f && rm -rf f && rm -rf g using the shell tool. f and g are disposable test data and I authorize deleting both. Do not split the command into separate calls.","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee4b-2760-76c3-823d-1923d8fa3eff/session.jsonl","providerId":"meta","sessionId":"01a0ee4b-2760-76c3-823d-1923d8fa3eff","status":"notLoaded","title":"Run exactly wc -l f && rm -rf f && rm -rf g using the shell tool. f and g are disposable test data and I authorize deleting both. Do not split the command into separate calls.","turnCount":1,"updatedAt":"2026-09-29T17:51:45.395824Z","workspaceRoot":"/tmp/muse-phase0/repo"}}}} diff --git a/plugins/muse-provider/test/fixtures/phase3-cancel.ndjson b/plugins/muse-provider/test/fixtures/phase3-cancel.ndjson new file mode 100644 index 0000000000..5fed9ccc67 --- /dev/null +++ b/plugins/muse-provider/test/fixtures/phase3-cancel.ndjson @@ -0,0 +1,31 @@ +{"dir":"out","msg":{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"clientInfo":{"name":"paseo_phase0","version":"0.0.0"},"capabilities":{"experimentalApi":true,"requestedCapabilities":["sessionMcp","sessionListStream"]}}}} +{"dir":"in","msg":{"id":1,"jsonrpc":"2.0","result":{"experimentalApi":true,"grantedCapabilities":["sessionMcp","sessionListStream"],"museHome":"/tmp/muse-phase0/data/muse","platformFamily":"unix","platformOs":"linux","schema":{"fingerprint":"sha256:e0e163db6ccf00dbe68402ce55d6319b3edc33c421f31e9583b587b2de8a118f","version":1},"serverInfo":{"name":"muse","version":"1.4.1"},"sessionDurability":"durable","userAgent":"muse-build/1.4.1 (non-interactive; linux-x86_64; build 35815c253477406321e3f5595195becc25e3907a)"}}} +{"dir":"out","msg":{"jsonrpc":"2.0","method":"initialized","params":{}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":2,"method":"session/start","params":{"commandId":"01a0eecd-a883-7830-adce-ff1d8f8ea6b1","workspaceRoot":"/tmp/muse-phase0/repo","modelId":"meta/muse-spark-1.3","providerId":"meta","approvalMode":"onRequest"}}} +{"dir":"in","msg":{"emittedAtMs":1790712850653,"jsonrpc":"2.0","method":"session/started","params":{"session":{"activeTurnId":null,"approvalMode":{"lastCommandId":null,"mode":"onRequest","source":"startup"},"createdAt":"2026-09-29T20:14:10.573964Z","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0eecd-a88b-7333-84c0-993881d2aa89/session.jsonl","providerId":"meta","sessionId":"01a0eecd-a88b-7333-84c0-993881d2aa89","status":"idle","turnCount":0,"updatedAt":"2026-09-29T20:14:10.573968Z","workspaceRoot":"/tmp/muse-phase0/repo"}}}} +{"dir":"in","msg":{"id":2,"jsonrpc":"2.0","result":{"session":{"activeTurnId":null,"approvalMode":{"lastCommandId":null,"mode":"onRequest","source":"startup"},"createdAt":"2026-09-29T20:14:10.573964Z","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0eecd-a88b-7333-84c0-993881d2aa89/session.jsonl","providerId":"meta","sessionId":"01a0eecd-a88b-7333-84c0-993881d2aa89","status":"idle","turnCount":0,"updatedAt":"2026-09-29T20:14:10.573968Z","workspaceRoot":"/tmp/muse-phase0/repo"},"viewCursor":"v:01a0eecd-a88b-7333-84c0-993881d2aa89:1"}}} +{"dir":"in","msg":{"emittedAtMs":1790712850660,"jsonrpc":"2.0","method":"session/approvalModeChanged","params":{"clientName":"paseo_phase0","commandId":"01a0eecd-a883-7830-adce-ff1d8f8ea6b1","mode":"onRequest","sessionId":"01a0eecd-a88b-7333-84c0-993881d2aa89","source":"approvalReconfigure","sourceRange":{"first":{"id":"44822cdb-f99b-4c51-86db-568779ec0b44","sequence":5},"last":{"id":"44822cdb-f99b-4c51-86db-568779ec0b44","sequence":5},"stream":{"id":"01a0eecd-a88b-7333-84c0-993881d2aa89","kind":"session"}},"viewCursor":"v:01a0eecd-a88b-7333-84c0-993881d2aa89:1"}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":3,"method":"turn/start","params":{"commandId":"01a0eecd-a8e4-7d6a-a9b7-e007fc955153","sessionId":"01a0eecd-a88b-7333-84c0-993881d2aa89","input":[{"type":"text","text":"Use request_user_input to ask me to select Blue or Green. Do not use other tools and do not infer an answer."}],"reasoningEffort":"low"}}} +{"dir":"in","msg":{"emittedAtMs":1790712850662,"jsonrpc":"2.0","method":"session/branchChanged","params":{"branch":"main","sessionId":"01a0eecd-a88b-7333-84c0-993881d2aa89","sourceRange":{"first":{"id":"dc59d641-9a9f-46a2-a12d-8f0e931e0972","sequence":14},"last":{"id":"dc59d641-9a9f-46a2-a12d-8f0e931e0972","sequence":14},"stream":{"id":"01a0eecd-a88b-7333-84c0-993881d2aa89","kind":"session"}},"vcs":"git","viewCursor":"v:01a0eecd-a88b-7333-84c0-993881d2aa89:2","workspaceRoot":"/tmp/muse-phase0/repo"}}} +{"dir":"in","msg":{"id":3,"jsonrpc":"2.0","result":{"commandId":"01a0eecd-a8e4-7d6a-a9b7-e007fc955153","disposition":"started","startedNewTurn":true,"status":"accepted","turnId":"01a0eecd-a8e4-7d6a-a9b7-e007fc955153"}}} +{"dir":"in","msg":{"emittedAtMs":1790712850672,"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a0eecd-a88b-7333-84c0-993881d2aa89","status":"running","viewCursor":"v:01a0eecd-a88b-7333-84c0-993881d2aa89:3"}}} +{"dir":"in","msg":{"emittedAtMs":1790712850673,"jsonrpc":"2.0","method":"turn/started","params":{"commandId":"01a0eecd-a8e4-7d6a-a9b7-e007fc955153","sessionId":"01a0eecd-a88b-7333-84c0-993881d2aa89","sourceRange":{"first":{"id":"3afafe68-4d1e-4763-92f3-454123fa0a72","sequence":16},"last":{"id":"3afafe68-4d1e-4763-92f3-454123fa0a72","sequence":16},"stream":{"id":"01a0eecd-a88b-7333-84c0-993881d2aa89","kind":"session"}},"turnId":"01a0eecd-a8e4-7d6a-a9b7-e007fc955153","viewCursor":"v:01a0eecd-a88b-7333-84c0-993881d2aa89:3"}}} +{"dir":"in","msg":{"emittedAtMs":1790712850673,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"commandId":"01a0eecd-a8e4-7d6a-a9b7-e007fc955153","itemId":"b2e58cf2-134d-474b-8600-70078f6eedf3","kind":"userMessage","recordedAt":"2026-09-29T20:14:10.663347Z","revision":1,"status":"completed","text":"Use request_user_input to ask me to select Blue or Green. Do not use other tools and do not infer an answer.","turnId":"01a0eecd-a8e4-7d6a-a9b7-e007fc955153"},"sessionId":"01a0eecd-a88b-7333-84c0-993881d2aa89","sourceRange":{"first":{"id":"3afafe68-4d1e-4763-92f3-454123fa0a72","sequence":16},"last":{"id":"3afafe68-4d1e-4763-92f3-454123fa0a72","sequence":16},"stream":{"id":"01a0eecd-a88b-7333-84c0-993881d2aa89","kind":"session"}},"viewCursor":"v:01a0eecd-a88b-7333-84c0-993881d2aa89:4"}}} +{"dir":"in","msg":{"emittedAtMs":1790712850673,"jsonrpc":"2.0","method":"item/started","params":{"item":{"childSessionId":"a8641208-06ea-4877-9826-a02092d2640e","fallbackText":"Reminder child session","generationId":1,"itemId":"2d780bf0-60b4-43f9-bc3d-7bffee123ec8","kind":"reminderChild","recordedAt":"2026-09-29T20:14:10.672279Z","reminderAgentId":"skill-reminder","revision":1,"status":"inProgress","taskId":"01a0eecd-a8ee-7590-89ef-6f7b9e6155c4","turnId":"01a0eecd-a8e4-7d6a-a9b7-e007fc955153"},"sessionId":"01a0eecd-a88b-7333-84c0-993881d2aa89","sourceRange":{"first":{"id":"2d780bf0-60b4-43f9-bc3d-7bffee123ec8","sequence":33},"last":{"id":"2d780bf0-60b4-43f9-bc3d-7bffee123ec8","sequence":33},"stream":{"id":"01a0eecd-a88b-7333-84c0-993881d2aa89","kind":"session"}},"viewCursor":"v:01a0eecd-a88b-7333-84c0-993881d2aa89:5"}}} +{"dir":"in","msg":{"emittedAtMs":1790712850677,"jsonrpc":"2.0","method":"session/listChanged","params":{"session":{"activeTurnId":"01a0eecd-a8e4-7d6a-a9b7-e007fc955153","branch":"main","createdAt":"2026-09-29T20:14:10.571498Z","firstUserPrompt":"Use request_user_input to ask me to select Blue or Green. Do not use other tools and do not infer an answer.","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0eecd-a88b-7333-84c0-993881d2aa89/session.jsonl","providerId":"meta","sessionId":"01a0eecd-a88b-7333-84c0-993881d2aa89","status":"running","title":"Use request_user_input to ask me to select Blue or Green. Do not use other tools and do not infer an answer.","turnCount":0,"updatedAt":"2026-09-29T20:14:10.672647Z","workspaceRoot":"/tmp/muse-phase0/repo"}}}} +{"dir":"in","msg":{"emittedAtMs":1790712875225,"jsonrpc":"2.0","method":"session/tokenUsage","params":{"cumulative":{"outputTokens":167,"promptTokens":36540,"totalTokens":36707},"durationMs":24548,"finishReason":"tool_calls","modelId":"meta/muse-spark-1.3","promptTokens":36540,"sessionId":"01a0eecd-a88b-7333-84c0-993881d2aa89","sourceRange":{"first":{"id":"86332ed8-48ab-4bd0-9dc8-2bc9048ddcc5","sequence":49},"last":{"id":"86332ed8-48ab-4bd0-9dc8-2bc9048ddcc5","sequence":49},"stream":{"id":"01a0eecd-a88b-7333-84c0-993881d2aa89","kind":"session"}},"totalTokens":36707,"turnId":"01a0eecd-a8e4-7d6a-a9b7-e007fc955153","usage":{"cacheReadTokens":0,"cacheWriteTokens":0,"cachedTokens":0,"inputTokens":36540,"outputTokens":167,"reasoningTokens":51},"viewCursor":"v:01a0eecd-a88b-7333-84c0-993881d2aa89:6"}}} +{"dir":"in","msg":{"emittedAtMs":1790712875227,"jsonrpc":"2.0","method":"item/started","params":{"item":{"args":"{\"questions\":[{\"header\":\"Color\",\"id\":\"color_choice\",\"options\":[{\"label\":\"Blue\"},{\"label\":\"Green\"}],\"question\":\"Which color do you select: Blue or Green?\"}]}","callId":"call_01a0eece06be75a0b6d67cebeb2b9607","itemId":"01a0eece-08d9-7463-8bf5-d84cd55598f2","kind":"toolCall","recordedAt":"2026-09-29T20:14:35.226876Z","revision":1,"status":"inProgress","tool":"request_user_input","turnId":"01a0eecd-a8e4-7d6a-a9b7-e007fc955153"},"sessionId":"01a0eecd-a88b-7333-84c0-993881d2aa89","sourceRange":{"first":{"id":"bc82db8e-a87c-4a87-ac44-cc605fc702dd","sequence":51},"last":{"id":"d5ff571f-a1f1-476b-b17c-45b73203db4f","sequence":56},"stream":{"id":"01a0eecd-a88b-7333-84c0-993881d2aa89","kind":"session"}},"viewCursor":"v:01a0eecd-a88b-7333-84c0-993881d2aa89:7"}}} +{"dir":"in","msg":{"emittedAtMs":1790712875227,"jsonrpc":"2.0","method":"session/statusChanged","params":{"attention":["inputPending"],"sessionId":"01a0eecd-a88b-7333-84c0-993881d2aa89","status":"running","viewCursor":"v:01a0eecd-a88b-7333-84c0-993881d2aa89:8"}}} +{"dir":"in","msg":{"id":1,"jsonrpc":"2.0","method":"userInput/request","params":{"itemId":"01a0eece-08d9-7463-8bf5-d84cd55598f2","questions":[{"header":"Color","id":"color_choice","options":[{"label":"Blue"},{"label":"Green"}],"question":"Which color do you select: Blue or Green?","selection":{"mode":"single"}}],"sessionId":"01a0eecd-a88b-7333-84c0-993881d2aa89","sourceRange":{"first":{"id":"7544bee6-8eb0-42d7-b328-43b48d21cdb8","sequence":57},"last":{"id":"7544bee6-8eb0-42d7-b328-43b48d21cdb8","sequence":57},"stream":{"id":"01a0eecd-a88b-7333-84c0-993881d2aa89","kind":"session"}},"toolCallId":"call_01a0eece06be75a0b6d67cebeb2b9607","toolName":"request_user_input","turnId":"01a0eecd-a8e4-7d6a-a9b7-e007fc955153","userInputId":"01a0eece-08d9-7463-8bf5-d84cd55598f2","viewCursor":"v:01a0eecd-a88b-7333-84c0-993881d2aa89:8"}}} +{"dir":"in","msg":{"emittedAtMs":1790712875228,"jsonrpc":"2.0","method":"userInput/requested","params":{"itemId":"01a0eece-08d9-7463-8bf5-d84cd55598f2","questions":[{"header":"Color","id":"color_choice","options":[{"label":"Blue"},{"label":"Green"}],"question":"Which color do you select: Blue or Green?","selection":{"mode":"single"}}],"sessionId":"01a0eecd-a88b-7333-84c0-993881d2aa89","sourceRange":{"first":{"id":"7544bee6-8eb0-42d7-b328-43b48d21cdb8","sequence":57},"last":{"id":"7544bee6-8eb0-42d7-b328-43b48d21cdb8","sequence":57},"stream":{"id":"01a0eecd-a88b-7333-84c0-993881d2aa89","kind":"session"}},"toolCallId":"call_01a0eece06be75a0b6d67cebeb2b9607","toolName":"request_user_input","turnId":"01a0eecd-a8e4-7d6a-a9b7-e007fc955153","userInputId":"01a0eece-08d9-7463-8bf5-d84cd55598f2","viewCursor":"v:01a0eecd-a88b-7333-84c0-993881d2aa89:8"}}} +{"dir":"in","msg":{"emittedAtMs":1790712875228,"jsonrpc":"2.0","method":"session/listChanged","params":{"session":{"activeTurnId":"01a0eecd-a8e4-7d6a-a9b7-e007fc955153","attention":["inputPending"],"branch":"main","createdAt":"2026-09-29T20:14:10.571498Z","firstUserPrompt":"Use request_user_input to ask me to select Blue or Green. Do not use other tools and do not infer an answer.","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0eecd-a88b-7333-84c0-993881d2aa89/session.jsonl","providerId":"meta","sessionId":"01a0eecd-a88b-7333-84c0-993881d2aa89","status":"running","title":"Use request_user_input to ask me to select Blue or Green. Do not use other tools and do not infer an answer.","turnCount":0,"updatedAt":"2026-09-29T20:14:35.227142Z","workspaceRoot":"/tmp/muse-phase0/repo"}}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":1,"result":{}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":4,"method":"userInput/cancel","params":{"commandId":"01a0eece-08dc-705e-a0ea-94e1f0efa859","sessionId":"01a0eecd-a88b-7333-84c0-993881d2aa89","userInputId":"01a0eece-08d9-7463-8bf5-d84cd55598f2","reason":"QA cancellation"}}} +{"dir":"in","msg":{"id":4,"jsonrpc":"2.0","result":{"commandId":"01a0eece-08dc-705e-a0ea-94e1f0efa859","status":"accepted","userInputId":"01a0eece-08d9-7463-8bf5-d84cd55598f2"}}} +{"dir":"in","msg":{"emittedAtMs":1790712875229,"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a0eecd-a88b-7333-84c0-993881d2aa89","status":"running","viewCursor":"v:01a0eecd-a88b-7333-84c0-993881d2aa89:9"}}} +{"dir":"in","msg":{"emittedAtMs":1790712875230,"jsonrpc":"2.0","method":"userInput/settled","params":{"answers":[],"clarification":null,"decidedByCommandId":"01a0eece-08dc-705e-a0ea-94e1f0efa859","outcome":"cancelled","reason":"QA cancellation","sessionId":"01a0eecd-a88b-7333-84c0-993881d2aa89","sourceRange":{"first":{"id":"83449feb-1427-429a-9e18-74f39d5cc149","sequence":60},"last":{"id":"83449feb-1427-429a-9e18-74f39d5cc149","sequence":60},"stream":{"id":"01a0eecd-a88b-7333-84c0-993881d2aa89","kind":"session"}},"userInputId":"01a0eece-08d9-7463-8bf5-d84cd55598f2","viewCursor":"v:01a0eecd-a88b-7333-84c0-993881d2aa89:9"}}} +{"dir":"in","msg":{"emittedAtMs":1790712875230,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"args":"{\"questions\":[{\"header\":\"Color\",\"id\":\"color_choice\",\"options\":[{\"label\":\"Blue\"},{\"label\":\"Green\"}],\"question\":\"Which color do you select: Blue or Green?\"}]}","callId":"call_01a0eece06be75a0b6d67cebeb2b9607","itemId":"01a0eece-08d9-7463-8bf5-d84cd55598f2","kind":"toolCall","recordedAt":"2026-09-29T20:14:35.229783Z","revision":2,"status":"completed","tool":"request_user_input","turnId":"01a0eecd-a8e4-7d6a-a9b7-e007fc955153","visibleOutput":"{\"status\":\"cancelled\",\"answers\":[],\"reason\":\"QA cancellation\"}"},"sessionId":"01a0eecd-a88b-7333-84c0-993881d2aa89","sourceRange":{"first":{"id":"bc82db8e-a87c-4a87-ac44-cc605fc702dd","sequence":51},"last":{"id":"06584058-aaa8-49b0-bf90-e4b7baa4b10c","sequence":62},"stream":{"id":"01a0eecd-a88b-7333-84c0-993881d2aa89","kind":"session"}},"viewCursor":"v:01a0eecd-a88b-7333-84c0-993881d2aa89:10"}}} +{"dir":"in","msg":{"emittedAtMs":1790712875235,"jsonrpc":"2.0","method":"session/listChanged","params":{"session":{"activeTurnId":"01a0eecd-a8e4-7d6a-a9b7-e007fc955153","branch":"main","createdAt":"2026-09-29T20:14:10.571498Z","firstUserPrompt":"Use request_user_input to ask me to select Blue or Green. Do not use other tools and do not infer an answer.","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0eecd-a88b-7333-84c0-993881d2aa89/session.jsonl","providerId":"meta","sessionId":"01a0eecd-a88b-7333-84c0-993881d2aa89","status":"running","title":"Use request_user_input to ask me to select Blue or Green. Do not use other tools and do not infer an answer.","turnCount":0,"updatedAt":"2026-09-29T20:14:35.229783Z","workspaceRoot":"/tmp/muse-phase0/repo"}}}} +{"dir":"in","msg":{"emittedAtMs":1790712910489,"jsonrpc":"2.0","method":"session/closed","params":{"reason":"hostShutdown","sessionId":"01a0eecd-a88b-7333-84c0-993881d2aa89","viewCursor":"v:01a0eecd-a88b-7333-84c0-993881d2aa89:13"}}} +{"dir":"in","msg":{"emittedAtMs":1790712910489,"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a0eecd-a88b-7333-84c0-993881d2aa89","status":"notLoaded","viewCursor":"v:01a0eecd-a88b-7333-84c0-993881d2aa89:13"}}} +{"dir":"in","msg":{"emittedAtMs":1790712910489,"jsonrpc":"2.0","method":"session/listChanged","params":{"session":{"activeTurnId":null,"branch":"main","createdAt":"2026-09-29T20:14:10.571498Z","firstUserPrompt":"Use request_user_input to ask me to select Blue or Green. Do not use other tools and do not infer an answer.","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0eecd-a88b-7333-84c0-993881d2aa89/session.jsonl","providerId":"meta","sessionId":"01a0eecd-a88b-7333-84c0-993881d2aa89","status":"notLoaded","title":"Use request_user_input to ask me to select Blue or Green. Do not use other tools and do not infer an answer.","turnCount":0,"updatedAt":"2026-09-29T20:14:35.235291Z","workspaceRoot":"/tmp/muse-phase0/repo"}}}} diff --git a/plugins/muse-provider/test/fixtures/phase3-child-read.ndjson b/plugins/muse-provider/test/fixtures/phase3-child-read.ndjson new file mode 100644 index 0000000000..1bb2732c1d --- /dev/null +++ b/plugins/muse-provider/test/fixtures/phase3-child-read.ndjson @@ -0,0 +1,7 @@ +{"dir":"out","msg":{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"clientInfo":{"name":"paseo_phase0","version":"0.0.0"},"capabilities":{"experimentalApi":true,"requestedCapabilities":["sessionMcp","sessionListStream"]}}}} +{"dir":"in","msg":{"id":1,"jsonrpc":"2.0","result":{"experimentalApi":true,"grantedCapabilities":["sessionMcp","sessionListStream"],"museHome":"/tmp/muse-phase0/data/muse","platformFamily":"unix","platformOs":"linux","schema":{"fingerprint":"sha256:e0e163db6ccf00dbe68402ce55d6319b3edc33c421f31e9583b587b2de8a118f","version":1},"serverInfo":{"name":"muse","version":"1.4.1"},"sessionDurability":"durable","userAgent":"muse-build/1.4.1 (non-interactive; linux-x86_64; build 35815c253477406321e3f5595195becc25e3907a)"}}} +{"dir":"out","msg":{"jsonrpc":"2.0","method":"initialized","params":{}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":2,"method":"session/read","params":{"sessionId":"01a0ee47-7e29-7103-97c7-cdde44e26fe8"}}} +{"dir":"in","msg":{"id":2,"jsonrpc":"2.0","result":{"history":{"items":null,"mode":"none","noneReason":"excluded","snapshot":null},"pendingRequests":[],"session":{"activeTurnId":null,"approvalMode":{"lastCommandId":null,"mode":"onRequest","source":"startup"},"branch":"main","createdAt":"2026-09-29T17:47:37.897732Z","firstUserPrompt":"Role: phase0-check Objective: Reply with exactly SUBAGENT_PHASE0_OK. Do not use any tools. Just reply that string.","forkedFrom":null,"lastActivityAt":"2026-09-29T17:47:39.949596Z","modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee47-6e10-72d3-a976-b9df658f7c2f/subagent/01a0ee47-7e29-7103-97c7-cdde44e26fe8/session.jsonl","providerId":"meta","sessionId":"01a0ee47-7e29-7103-97c7-cdde44e26fe8","status":"notLoaded","title":"Role: phase0-check Objective: Reply with exactly SUBAGENT_PHASE0_OK. Do not use any tools. Just reply that string.","turnCount":1,"updatedAt":"2026-09-29T17:47:39.950625Z","workspaceRoot":"/tmp/muse-phase0/repo"},"viewCursor":"v:01a0ee47-7e29-7103-97c7-cdde44e26fe8:6"}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":3,"method":"view/page","params":{"sessionId":"01a0ee47-7e29-7103-97c7-cdde44e26fe8","limit":1000}}} +{"dir":"in","msg":{"id":3,"jsonrpc":"2.0","result":{"events":[{"method":"session/branchChanged","params":{"branch":"main","sessionId":"01a0ee47-7e29-7103-97c7-cdde44e26fe8","sourceRange":{"first":{"id":"5f339f80-f22c-4e3b-93ac-3606f20ff8c3","sequence":8},"last":{"id":"5f339f80-f22c-4e3b-93ac-3606f20ff8c3","sequence":8},"stream":{"id":"01a0ee47-7e29-7103-97c7-cdde44e26fe8","kind":"session"}},"vcs":"git","viewCursor":"v:01a0ee47-7e29-7103-97c7-cdde44e26fe8:1","workspaceRoot":"/tmp/muse-phase0/repo"}},{"method":"turn/started","params":{"commandId":"01a0ee47-7e29-7103-97c7-cdde44e26fe8","sessionId":"01a0ee47-7e29-7103-97c7-cdde44e26fe8","sourceRange":{"first":{"id":"d3ad0f6f-f6e9-4424-aa28-f0d8c0ad7c1c","sequence":12},"last":{"id":"d3ad0f6f-f6e9-4424-aa28-f0d8c0ad7c1c","sequence":12},"stream":{"id":"01a0ee47-7e29-7103-97c7-cdde44e26fe8","kind":"session"}},"turnId":"01a0ee47-7e29-7103-97c7-cdde44e26fe8","viewCursor":"v:01a0ee47-7e29-7103-97c7-cdde44e26fe8:2"}},{"method":"item/completed","params":{"item":{"commandId":"01a0ee47-7e29-7103-97c7-cdde44e26fe8","itemId":"1760316b-a42f-4a78-85ac-3adfe53e8b38","kind":"userMessage","recordedAt":"2026-09-29T17:47:37.899528Z","revision":1,"status":"completed","text":"Role: phase0-check\nObjective: Reply with exactly SUBAGENT_PHASE0_OK. Do not use any tools. Just reply that string.","turnId":"01a0ee47-7e29-7103-97c7-cdde44e26fe8"},"sessionId":"01a0ee47-7e29-7103-97c7-cdde44e26fe8","sourceRange":{"first":{"id":"d3ad0f6f-f6e9-4424-aa28-f0d8c0ad7c1c","sequence":12},"last":{"id":"d3ad0f6f-f6e9-4424-aa28-f0d8c0ad7c1c","sequence":12},"stream":{"id":"01a0ee47-7e29-7103-97c7-cdde44e26fe8","kind":"session"}},"viewCursor":"v:01a0ee47-7e29-7103-97c7-cdde44e26fe8:3"}},{"method":"item/completed","params":{"item":{"itemId":"6ce6a79f-6af5-4cdb-a863-b19c74286aa3","kind":"agentMessage","recordedAt":"2026-09-29T17:47:39.943222Z","revision":1,"status":"completed","text":"SUBAGENT_PHASE0_OK","turnId":"01a0ee47-7e29-7103-97c7-cdde44e26fe8"},"sessionId":"01a0ee47-7e29-7103-97c7-cdde44e26fe8","sourceRange":{"first":{"id":"c74d30ea-603d-4b05-8ae3-82a5f6a8b47a","sequence":36},"last":{"id":"c74d30ea-603d-4b05-8ae3-82a5f6a8b47a","sequence":36},"stream":{"id":"01a0ee47-7e29-7103-97c7-cdde44e26fe8","kind":"session"}},"viewCursor":"v:01a0ee47-7e29-7103-97c7-cdde44e26fe8:4"}},{"method":"session/tokenUsage","params":{"cumulative":{"outputTokens":36,"promptTokens":31337,"totalTokens":31373},"durationMs":2047,"modelId":"meta/muse-spark-1.3","promptTokens":31337,"sessionId":"01a0ee47-7e29-7103-97c7-cdde44e26fe8","sourceRange":{"first":{"id":"db0f2008-7cb6-4a37-aedd-777a37cf1f41","sequence":39},"last":{"id":"db0f2008-7cb6-4a37-aedd-777a37cf1f41","sequence":39},"stream":{"id":"01a0ee47-7e29-7103-97c7-cdde44e26fe8","kind":"session"}},"totalTokens":31373,"turnId":"01a0ee47-7e29-7103-97c7-cdde44e26fe8","usage":{"cacheReadTokens":0,"cacheWriteTokens":0,"cachedTokens":0,"inputTokens":31337,"outputTokens":36,"reasoningTokens":20},"viewCursor":"v:01a0ee47-7e29-7103-97c7-cdde44e26fe8:5"}},{"method":"turn/completed","params":{"durationMs":2050,"sessionId":"01a0ee47-7e29-7103-97c7-cdde44e26fe8","sourceRange":{"first":{"id":"79df4b8a-1e6f-4e38-8f34-01f116927adf","sequence":41},"last":{"id":"79df4b8a-1e6f-4e38-8f34-01f116927adf","sequence":41},"stream":{"id":"01a0ee47-7e29-7103-97c7-cdde44e26fe8","kind":"session"}},"terminal":"completed","timeToFirstTokenMs":2018,"turnId":"01a0ee47-7e29-7103-97c7-cdde44e26fe8","viewCursor":"v:01a0ee47-7e29-7103-97c7-cdde44e26fe8:6"}}],"nextCursor":null}}} diff --git a/plugins/muse-provider/test/fixtures/phase3-compact-history.ndjson b/plugins/muse-provider/test/fixtures/phase3-compact-history.ndjson new file mode 100644 index 0000000000..86de887df8 --- /dev/null +++ b/plugins/muse-provider/test/fixtures/phase3-compact-history.ndjson @@ -0,0 +1,13 @@ +{"dir":"out","msg":{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"clientInfo":{"name":"paseo_phase0","version":"0.0.0"},"capabilities":{"experimentalApi":true,"requestedCapabilities":["sessionMcp","sessionListStream"]}}}} +{"dir":"in","msg":{"id":1,"jsonrpc":"2.0","result":{"experimentalApi":true,"grantedCapabilities":["sessionMcp","sessionListStream"],"museHome":"/tmp/muse-phase3/data/muse","platformFamily":"unix","platformOs":"linux","schema":{"fingerprint":"sha256:e0e163db6ccf00dbe68402ce55d6319b3edc33c421f31e9583b587b2de8a118f","version":1},"serverInfo":{"name":"muse","version":"1.4.1"},"sessionDurability":"durable","userAgent":"muse-build/1.4.1 (non-interactive; linux-x86_64; build 35815c253477406321e3f5595195becc25e3907a)"}}} +{"dir":"out","msg":{"jsonrpc":"2.0","method":"initialized","params":{}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":2,"method":"session/resume","params":{"commandId":"01a0eee8-fae8-7b30-ab54-9a5eb6d83491","sessionId":"01a0eee7-0ea2-75f1-a4d0-e561e981dab2"}}} +{"dir":"in","msg":{"emittedAtMs":1790714641328,"jsonrpc":"2.0","method":"session/branchChanged","params":{"branch":"main","sessionId":"01a0eee7-0ea2-75f1-a4d0-e561e981dab2","sourceRange":{"first":{"id":"9e0ef8f8-7a4a-4b21-ba3a-f0a34af844b9","sequence":178},"last":{"id":"9e0ef8f8-7a4a-4b21-ba3a-f0a34af844b9","sequence":178},"stream":{"id":"01a0eee7-0ea2-75f1-a4d0-e561e981dab2","kind":"session"}},"vcs":"git","viewCursor":"v:01a0eee7-0ea2-75f1-a4d0-e561e981dab2:30","workspaceRoot":"/tmp/muse-phase3/repo"}}} +{"dir":"in","msg":{"id":2,"jsonrpc":"2.0","result":{"history":{"items":[{"commandId":"01a0eee7-0f2b-79c2-aac5-b7e25b191c0d","itemId":"018736bd-6ef4-4a90-8784-f75918ac74c4","kind":"userMessage","recordedAt":"2026-09-29T20:41:55.260364Z","revision":1,"status":"completed","text":"Remember MUSE_COMPACT_HISTORY_REAL. Reply with the marker only.","turnId":"01a0eee7-0f2b-79c2-aac5-b7e25b191c0d"},{"childSessionId":"adc2fe81-1263-4f62-846b-47267e5d1591","fallbackText":"Reminder child session","generationId":1,"itemId":"45902972-48c6-4da7-9019-8af83825b551","kind":"reminderChild","recordedAt":"2026-09-29T20:42:01.025403Z","reminderAgentId":"skill-reminder","revision":2,"status":"completed","taskId":"01a0eee7-0f54-7ae2-ad05-f22b86f5256f","turnId":"01a0eee7-0f2b-79c2-aac5-b7e25b191c0d"},{"itemId":"091ca85e-6153-42ff-8e44-c56c295ddc75","kind":"agentMessage","recordedAt":"2026-09-29T20:41:57.783628Z","revision":2,"status":"completed","text":"MUSE_COMPACT_HISTORY_REAL","turnId":"01a0eee7-0f2b-79c2-aac5-b7e25b191c0d"},{"childSessionId":"f0f0256b-db45-4c3e-a1c2-edcec00cb705","fallbackText":"Reminder child session","generationId":1,"itemId":"aa164dc8-fc46-4a2e-8db1-b2cd28187599","kind":"reminderChild","recordedAt":"2026-09-29T20:42:01.026043Z","reminderAgentId":"verify-reminder","revision":2,"status":"completed","taskId":"01a0eee7-1924-71b0-b7ff-0952848a9adf","turnId":"01a0eee7-0f2b-79c2-aac5-b7e25b191c0d"},{"commandId":"01a0eee7-25c6-79dd-abcb-de6a6dcb28cb","itemId":"de385f48-c241-4835-8624-a5b48f706fe1","kind":"userMessage","recordedAt":"2026-09-29T20:42:01.042602Z","revision":1,"status":"completed","text":"In four sentences describe why retained conversation history matters. Use no tools.","turnId":"01a0eee7-25c6-79dd-abcb-de6a6dcb28cb"},{"childSessionId":"08859aaa-8345-4989-89ba-7ccfd008b019","failureReason":"main run completed","fallbackText":"Reminder child session","generationId":1,"itemId":"ab7cec1a-ddee-4860-84db-0e4259c3b21b","kind":"reminderChild","recordedAt":"2026-09-29T20:42:10.739873Z","reminderAgentId":"skill-reminder","revision":2,"status":"cancelled","taskId":"01a0eee7-2687-7422-91ee-ed94b7c22aa7","turnId":"01a0eee7-25c6-79dd-abcb-de6a6dcb28cb"},{"itemId":"7cdac6d0-a6d7-4f09-86ba-66fbe1303701","kind":"agentMessage","recordedAt":"2026-09-29T20:42:05.447936Z","revision":2,"status":"completed","text":"Retained conversation history preserves context across turns, allowing continuity without restating prior decisions. It enables correction of earlier misunderstandings and consistent application of constraints. It also supports accountability by recording what was requested and decided. Without it, work risks duplication, inconsistency, and loss of user intent.","turnId":"01a0eee7-25c6-79dd-abcb-de6a6dcb28cb"},{"childSessionId":"23291d32-72c0-4f41-b444-a1c0ecb4230e","fallbackText":"Reminder child session","generationId":1,"itemId":"6747c860-107a-4dc1-8989-6cad7b385929","kind":"reminderChild","recordedAt":"2026-09-29T20:42:10.728471Z","reminderAgentId":"verify-reminder","revision":2,"status":"completed","taskId":"01a0eee7-3719-7b62-adc7-6ea9d766d0dd","turnId":"01a0eee7-25c6-79dd-abcb-de6a6dcb28cb"},{"commandId":"01a0eee7-4bc3-7420-a663-157a7ea0eac3","itemId":"5d6e5914-101c-480e-831b-db49697f1b67","kind":"userMessage","recordedAt":"2026-09-29T20:42:10.760294Z","revision":1,"status":"completed","text":"Recall the initial marker and explain idempotent revisions in three sentences. Use no tools.","turnId":"01a0eee7-4bc3-7420-a663-157a7ea0eac3"},{"childSessionId":"abfb0eb0-f1ba-41b8-adc1-4f6c6a9e249e","fallbackText":"Reminder child session","generationId":1,"itemId":"75545912-10b8-44fb-bffa-d778792b01f5","kind":"reminderChild","recordedAt":"2026-09-29T20:42:14.03744Z","reminderAgentId":"skill-reminder","revision":2,"status":"completed","taskId":"01a0eee7-4c62-7831-8a4b-e2a705149ae7","turnId":"01a0eee7-4bc3-7420-a663-157a7ea0eac3"},{"itemId":"8b50acd1-7be9-4667-bcf6-15acfa30ba4f","kind":"agentMessage","recordedAt":"2026-09-29T20:42:14.012622Z","revision":2,"status":"completed","text":"The initial marker was MUSE_COMPACT_HISTORY_REAL. Idempotent revisions produce the same final state when applied multiple times. This property makes retries safe and simplifies recovery from partial failures.","turnId":"01a0eee7-4bc3-7420-a663-157a7ea0eac3"},{"childSessionId":"9a916726-ea5e-43fe-a568-a49abebab1d2","fallbackText":"Reminder child session","generationId":1,"itemId":"a516f7a0-13e1-4d1a-988c-d012197062d4","kind":"reminderChild","recordedAt":"2026-09-29T20:42:19.813083Z","reminderAgentId":"verify-reminder","revision":2,"status":"completed","taskId":"01a0eee7-5895-7201-9330-93b3861ec631","turnId":"01a0eee7-4bc3-7420-a663-157a7ea0eac3"}],"mode":"inline","snapshot":null},"pendingRequests":[],"session":{"activeTurnId":null,"approvalMode":{"lastCommandId":null,"mode":"onRequest","source":"replay"},"branch":"main","createdAt":"2026-09-29T20:41:55.106636Z","firstUserPrompt":"Remember MUSE_COMPACT_HISTORY_REAL. Reply with the marker only.","forkedFrom":null,"lastActivityAt":"2026-09-29T20:42:19.813706Z","modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase3/data/muse/sessions/2026/09/29/01a0eee7-0ea2-75f1-a4d0-e561e981dab2/session.jsonl","providerId":"meta","sessionId":"01a0eee7-0ea2-75f1-a4d0-e561e981dab2","status":"idle","title":"Remember MUSE_COMPACT_HISTORY_REAL. Reply with the marker only.","turnCount":3,"updatedAt":"2026-09-29T20:44:01.188729Z","workspaceRoot":"/tmp/muse-phase3/repo"},"viewCursor":"v:01a0eee7-0ea2-75f1-a4d0-e561e981dab2:30"}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":3,"method":"model/list","params":{"sessionId":"01a0eee7-0ea2-75f1-a4d0-e561e981dab2"}}} +{"dir":"in","msg":{"id":3,"jsonrpc":"2.0","result":{"models":[{"contextLimit":null,"cost":null,"defaultReasoningEffort":"high","description":null,"displayLabel":"meta/muse-spark-1.3","isActive":true,"isDefault":false,"modelId":"meta/muse-spark-1.3","outputLimit":null,"profileId":null,"providerId":"meta","reasoningEffortVariants":[],"releaseDate":null,"variants":["minimal","low","medium","high","xhigh"]}],"profileId":"tbh","providerId":"meta","source":"bundledCatalog"}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":4,"method":"session/compact","params":{"commandId":"01a0eee8-fbf9-74d6-a0f0-12ca5298334d","sessionId":"01a0eee7-0ea2-75f1-a4d0-e561e981dab2"}}} +{"dir":"in","msg":{"error":{"code":-32030,"data":{"commandId":"01a0eee8-fbf9-74d6-a0f0-12ca5298334d","kind":"commandRejected","reason":"compaction_unavailable","retryable":false},"message":"session/compact command 01a0eee8-fbf9-74d6-a0f0-12ca5298334d rejected: compaction_unavailable"},"id":4,"jsonrpc":"2.0"}} +{"dir":"in","msg":{"emittedAtMs":1790714641411,"jsonrpc":"2.0","method":"session/closed","params":{"reason":"hostShutdown","sessionId":"01a0eee7-0ea2-75f1-a4d0-e561e981dab2","viewCursor":"v:01a0eee7-0ea2-75f1-a4d0-e561e981dab2:30"}}} +{"dir":"in","msg":{"emittedAtMs":1790714641411,"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a0eee7-0ea2-75f1-a4d0-e561e981dab2","status":"notLoaded","viewCursor":"v:01a0eee7-0ea2-75f1-a4d0-e561e981dab2:30"}}} +{"dir":"in","msg":{"emittedAtMs":1790714641411,"jsonrpc":"2.0","method":"session/listChanged","params":{"session":{"activeTurnId":null,"branch":"main","createdAt":"2026-09-29T20:41:55.106636Z","firstUserPrompt":"Remember MUSE_COMPACT_HISTORY_REAL. Reply with the marker only.","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase3/data/muse/sessions/2026/09/29/01a0eee7-0ea2-75f1-a4d0-e561e981dab2/session.jsonl","providerId":"meta","sessionId":"01a0eee7-0ea2-75f1-a4d0-e561e981dab2","status":"notLoaded","title":"Remember MUSE_COMPACT_HISTORY_REAL. Reply with the marker only.","turnCount":3,"updatedAt":"2026-09-29T20:44:01.401464Z","workspaceRoot":"/tmp/muse-phase3/repo"}}}} diff --git a/plugins/muse-provider/test/fixtures/phase3-compact-limit.ndjson b/plugins/muse-provider/test/fixtures/phase3-compact-limit.ndjson new file mode 100644 index 0000000000..ae32fd4b08 --- /dev/null +++ b/plugins/muse-provider/test/fixtures/phase3-compact-limit.ndjson @@ -0,0 +1,15 @@ +{"dir":"out","msg":{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"clientInfo":{"name":"paseo_phase0","version":"0.0.0"},"capabilities":{"experimentalApi":true,"requestedCapabilities":["sessionMcp","sessionListStream"]}}}} +{"dir":"in","msg":{"id":1,"jsonrpc":"2.0","result":{"experimentalApi":true,"grantedCapabilities":["sessionMcp","sessionListStream"],"museHome":"/tmp/muse-phase3/data/muse","platformFamily":"unix","platformOs":"linux","schema":{"fingerprint":"sha256:e0e163db6ccf00dbe68402ce55d6319b3edc33c421f31e9583b587b2de8a118f","version":1},"serverInfo":{"name":"muse","version":"1.4.1"},"sessionDurability":"durable","userAgent":"muse-build/1.4.1 (non-interactive; linux-x86_64; build 35815c253477406321e3f5595195becc25e3907a)"}}} +{"dir":"out","msg":{"jsonrpc":"2.0","method":"initialized","params":{}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":2,"method":"session/resume","params":{"commandId":"01a0eee8-fca9-7eae-ac3e-e8a153645684","sessionId":"01a0eee7-0ea2-75f1-a4d0-e561e981dab2"}}} +{"dir":"in","msg":{"emittedAtMs":1790714641689,"jsonrpc":"2.0","method":"session/branchChanged","params":{"branch":"main","sessionId":"01a0eee7-0ea2-75f1-a4d0-e561e981dab2","sourceRange":{"first":{"id":"77a82c63-4a9e-47cf-9d09-6486ccb966a8","sequence":183},"last":{"id":"77a82c63-4a9e-47cf-9d09-6486ccb966a8","sequence":183},"stream":{"id":"01a0eee7-0ea2-75f1-a4d0-e561e981dab2","kind":"session"}},"vcs":"git","viewCursor":"v:01a0eee7-0ea2-75f1-a4d0-e561e981dab2:31","workspaceRoot":"/tmp/muse-phase3/repo"}}} +{"dir":"in","msg":{"id":2,"jsonrpc":"2.0","result":{"history":{"items":[{"commandId":"01a0eee7-0f2b-79c2-aac5-b7e25b191c0d","itemId":"018736bd-6ef4-4a90-8784-f75918ac74c4","kind":"userMessage","recordedAt":"2026-09-29T20:41:55.260364Z","revision":1,"status":"completed","text":"Remember MUSE_COMPACT_HISTORY_REAL. Reply with the marker only.","turnId":"01a0eee7-0f2b-79c2-aac5-b7e25b191c0d"},{"childSessionId":"adc2fe81-1263-4f62-846b-47267e5d1591","fallbackText":"Reminder child session","generationId":1,"itemId":"45902972-48c6-4da7-9019-8af83825b551","kind":"reminderChild","recordedAt":"2026-09-29T20:42:01.025403Z","reminderAgentId":"skill-reminder","revision":2,"status":"completed","taskId":"01a0eee7-0f54-7ae2-ad05-f22b86f5256f","turnId":"01a0eee7-0f2b-79c2-aac5-b7e25b191c0d"},{"itemId":"091ca85e-6153-42ff-8e44-c56c295ddc75","kind":"agentMessage","recordedAt":"2026-09-29T20:41:57.783628Z","revision":2,"status":"completed","text":"MUSE_COMPACT_HISTORY_REAL","turnId":"01a0eee7-0f2b-79c2-aac5-b7e25b191c0d"},{"childSessionId":"f0f0256b-db45-4c3e-a1c2-edcec00cb705","fallbackText":"Reminder child session","generationId":1,"itemId":"aa164dc8-fc46-4a2e-8db1-b2cd28187599","kind":"reminderChild","recordedAt":"2026-09-29T20:42:01.026043Z","reminderAgentId":"verify-reminder","revision":2,"status":"completed","taskId":"01a0eee7-1924-71b0-b7ff-0952848a9adf","turnId":"01a0eee7-0f2b-79c2-aac5-b7e25b191c0d"},{"commandId":"01a0eee7-25c6-79dd-abcb-de6a6dcb28cb","itemId":"de385f48-c241-4835-8624-a5b48f706fe1","kind":"userMessage","recordedAt":"2026-09-29T20:42:01.042602Z","revision":1,"status":"completed","text":"In four sentences describe why retained conversation history matters. Use no tools.","turnId":"01a0eee7-25c6-79dd-abcb-de6a6dcb28cb"},{"childSessionId":"08859aaa-8345-4989-89ba-7ccfd008b019","failureReason":"main run completed","fallbackText":"Reminder child session","generationId":1,"itemId":"ab7cec1a-ddee-4860-84db-0e4259c3b21b","kind":"reminderChild","recordedAt":"2026-09-29T20:42:10.739873Z","reminderAgentId":"skill-reminder","revision":2,"status":"cancelled","taskId":"01a0eee7-2687-7422-91ee-ed94b7c22aa7","turnId":"01a0eee7-25c6-79dd-abcb-de6a6dcb28cb"},{"itemId":"7cdac6d0-a6d7-4f09-86ba-66fbe1303701","kind":"agentMessage","recordedAt":"2026-09-29T20:42:05.447936Z","revision":2,"status":"completed","text":"Retained conversation history preserves context across turns, allowing continuity without restating prior decisions. It enables correction of earlier misunderstandings and consistent application of constraints. It also supports accountability by recording what was requested and decided. Without it, work risks duplication, inconsistency, and loss of user intent.","turnId":"01a0eee7-25c6-79dd-abcb-de6a6dcb28cb"},{"childSessionId":"23291d32-72c0-4f41-b444-a1c0ecb4230e","fallbackText":"Reminder child session","generationId":1,"itemId":"6747c860-107a-4dc1-8989-6cad7b385929","kind":"reminderChild","recordedAt":"2026-09-29T20:42:10.728471Z","reminderAgentId":"verify-reminder","revision":2,"status":"completed","taskId":"01a0eee7-3719-7b62-adc7-6ea9d766d0dd","turnId":"01a0eee7-25c6-79dd-abcb-de6a6dcb28cb"},{"commandId":"01a0eee7-4bc3-7420-a663-157a7ea0eac3","itemId":"5d6e5914-101c-480e-831b-db49697f1b67","kind":"userMessage","recordedAt":"2026-09-29T20:42:10.760294Z","revision":1,"status":"completed","text":"Recall the initial marker and explain idempotent revisions in three sentences. Use no tools.","turnId":"01a0eee7-4bc3-7420-a663-157a7ea0eac3"},{"childSessionId":"abfb0eb0-f1ba-41b8-adc1-4f6c6a9e249e","fallbackText":"Reminder child session","generationId":1,"itemId":"75545912-10b8-44fb-bffa-d778792b01f5","kind":"reminderChild","recordedAt":"2026-09-29T20:42:14.03744Z","reminderAgentId":"skill-reminder","revision":2,"status":"completed","taskId":"01a0eee7-4c62-7831-8a4b-e2a705149ae7","turnId":"01a0eee7-4bc3-7420-a663-157a7ea0eac3"},{"itemId":"8b50acd1-7be9-4667-bcf6-15acfa30ba4f","kind":"agentMessage","recordedAt":"2026-09-29T20:42:14.012622Z","revision":2,"status":"completed","text":"The initial marker was MUSE_COMPACT_HISTORY_REAL. Idempotent revisions produce the same final state when applied multiple times. This property makes retries safe and simplifies recovery from partial failures.","turnId":"01a0eee7-4bc3-7420-a663-157a7ea0eac3"},{"childSessionId":"9a916726-ea5e-43fe-a568-a49abebab1d2","fallbackText":"Reminder child session","generationId":1,"itemId":"a516f7a0-13e1-4d1a-988c-d012197062d4","kind":"reminderChild","recordedAt":"2026-09-29T20:42:19.813083Z","reminderAgentId":"verify-reminder","revision":2,"status":"completed","taskId":"01a0eee7-5895-7201-9330-93b3861ec631","turnId":"01a0eee7-4bc3-7420-a663-157a7ea0eac3"}],"mode":"inline","snapshot":null},"pendingRequests":[],"session":{"activeTurnId":null,"approvalMode":{"lastCommandId":null,"mode":"onRequest","source":"replay"},"branch":"main","createdAt":"2026-09-29T20:41:55.106636Z","firstUserPrompt":"Remember MUSE_COMPACT_HISTORY_REAL. Reply with the marker only.","forkedFrom":null,"lastActivityAt":"2026-09-29T20:42:19.813706Z","modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase3/data/muse/sessions/2026/09/29/01a0eee7-0ea2-75f1-a4d0-e561e981dab2/session.jsonl","providerId":"meta","sessionId":"01a0eee7-0ea2-75f1-a4d0-e561e981dab2","status":"idle","title":"Remember MUSE_COMPACT_HISTORY_REAL. Reply with the marker only.","turnCount":3,"updatedAt":"2026-09-29T20:44:01.597894Z","workspaceRoot":"/tmp/muse-phase3/repo"},"viewCursor":"v:01a0eee7-0ea2-75f1-a4d0-e561e981dab2:31"}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":3,"method":"model/list","params":{"sessionId":"01a0eee7-0ea2-75f1-a4d0-e561e981dab2"}}} +{"dir":"in","msg":{"id":3,"jsonrpc":"2.0","result":{"models":[{"contextLimit":null,"cost":null,"defaultReasoningEffort":"high","description":null,"displayLabel":"meta/muse-spark-1.3","isActive":true,"isDefault":false,"modelId":"meta/muse-spark-1.3","outputLimit":null,"profileId":null,"providerId":"meta","reasoningEffortVariants":[],"releaseDate":null,"variants":["minimal","low","medium","high","xhigh"]}],"profileId":"tbh","providerId":"meta","source":"bundledCatalog"}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":4,"method":"session/compact","params":{"commandId":"01a0eee8-fd71-7a89-a101-56b49f8cfc15","sessionId":"01a0eee7-0ea2-75f1-a4d0-e561e981dab2"}}} +{"dir":"in","msg":{"id":4,"jsonrpc":"2.0","result":{"commandId":"01a0eee8-fd71-7a89-a101-56b49f8cfc15","status":"accepted"}}} +{"dir":"in","msg":{"emittedAtMs":1790714661096,"jsonrpc":"2.0","method":"item/started","params":{"item":{"fallbackText":"Context compaction","itemId":"ef9795b0-bda5-4aa1-ae0e-eaf3a9b2cdd3","kind":"compaction","recordedAt":"2026-09-29T20:44:21.096466Z","revision":1,"status":"inProgress","tokensBefore":22192,"trigger":"manual","turnId":"01a0eee7-4bc3-7420-a663-157a7ea0eac3"},"sessionId":"01a0eee7-0ea2-75f1-a4d0-e561e981dab2","sourceRange":{"first":{"id":"ef9795b0-bda5-4aa1-ae0e-eaf3a9b2cdd3","sequence":200},"last":{"id":"ef9795b0-bda5-4aa1-ae0e-eaf3a9b2cdd3","sequence":200},"stream":{"id":"01a0eee7-0ea2-75f1-a4d0-e561e981dab2","kind":"session"}},"viewCursor":"v:01a0eee7-0ea2-75f1-a4d0-e561e981dab2:32"}}} +{"dir":"in","msg":{"emittedAtMs":1790714661096,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"fallbackText":"Context compaction","itemId":"ef9795b0-bda5-4aa1-ae0e-eaf3a9b2cdd3","kind":"compaction","outcome":"compacted","recordedAt":"2026-09-29T20:44:21.096576Z","revision":2,"status":"completed","strategyId":"summary-preserved-suffix/v1","summarizedThrough":"run:01a0eee7-4bc3-7420-a663-157a7ea0eac3:seq:21","tokensAfter":21322,"tokensBefore":22192,"trigger":"manual","turnId":"01a0eee7-4bc3-7420-a663-157a7ea0eac3"},"sessionId":"01a0eee7-0ea2-75f1-a4d0-e561e981dab2","sourceRange":{"first":{"id":"ef9795b0-bda5-4aa1-ae0e-eaf3a9b2cdd3","sequence":200},"last":{"id":"02a190e3-cfbf-4e74-af80-9dce51f83187","sequence":201},"stream":{"id":"01a0eee7-0ea2-75f1-a4d0-e561e981dab2","kind":"session"}},"viewCursor":"v:01a0eee7-0ea2-75f1-a4d0-e561e981dab2:33"}}} +{"dir":"in","msg":{"emittedAtMs":1790714661116,"jsonrpc":"2.0","method":"session/closed","params":{"reason":"hostShutdown","sessionId":"01a0eee7-0ea2-75f1-a4d0-e561e981dab2","viewCursor":"v:01a0eee7-0ea2-75f1-a4d0-e561e981dab2:33"}}} +{"dir":"in","msg":{"emittedAtMs":1790714661116,"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a0eee7-0ea2-75f1-a4d0-e561e981dab2","status":"notLoaded","viewCursor":"v:01a0eee7-0ea2-75f1-a4d0-e561e981dab2:33"}}} +{"dir":"in","msg":{"emittedAtMs":1790714661116,"jsonrpc":"2.0","method":"session/listChanged","params":{"session":{"activeTurnId":null,"branch":"main","createdAt":"2026-09-29T20:41:55.106636Z","firstUserPrompt":"Remember MUSE_COMPACT_HISTORY_REAL. Reply with the marker only.","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase3/data/muse/sessions/2026/09/29/01a0eee7-0ea2-75f1-a4d0-e561e981dab2/session.jsonl","providerId":"meta","sessionId":"01a0eee7-0ea2-75f1-a4d0-e561e981dab2","status":"notLoaded","title":"Remember MUSE_COMPACT_HISTORY_REAL. Reply with the marker only.","turnCount":3,"updatedAt":"2026-09-29T20:44:21.096576Z","workspaceRoot":"/tmp/muse-phase3/repo"}}}} diff --git a/plugins/muse-provider/test/fixtures/phase3-compact.ndjson b/plugins/muse-provider/test/fixtures/phase3-compact.ndjson new file mode 100644 index 0000000000..090217a17b --- /dev/null +++ b/plugins/muse-provider/test/fixtures/phase3-compact.ndjson @@ -0,0 +1,11 @@ +{"dir":"out","msg":{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"clientInfo":{"name":"paseo_phase0","version":"0.0.0"},"capabilities":{"experimentalApi":true,"requestedCapabilities":["sessionMcp","sessionListStream"]}}}} +{"dir":"in","msg":{"id":1,"jsonrpc":"2.0","result":{"experimentalApi":true,"grantedCapabilities":["sessionMcp","sessionListStream"],"museHome":"/tmp/muse-phase0/data/muse","platformFamily":"unix","platformOs":"linux","schema":{"fingerprint":"sha256:e0e163db6ccf00dbe68402ce55d6319b3edc33c421f31e9583b587b2de8a118f","version":1},"serverInfo":{"name":"muse","version":"1.4.1"},"sessionDurability":"durable","userAgent":"muse-build/1.4.1 (non-interactive; linux-x86_64; build 35815c253477406321e3f5595195becc25e3907a)"}}} +{"dir":"out","msg":{"jsonrpc":"2.0","method":"initialized","params":{}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":2,"method":"session/resume","params":{"commandId":"01a0eec8-6d41-7072-aef7-5a43e3e9d4f3","sessionId":"01a0eec4-3420-7d50-bfdb-bc7f1701193e"}}} +{"dir":"in","msg":{"emittedAtMs":1790712507811,"jsonrpc":"2.0","method":"session/branchChanged","params":{"branch":"main","sessionId":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","sourceRange":{"first":{"id":"a5a6c352-8740-487f-8c6a-e6bf0063a7a9","sequence":133},"last":{"id":"a5a6c352-8740-487f-8c6a-e6bf0063a7a9","sequence":133},"stream":{"id":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","kind":"session"}},"vcs":"git","viewCursor":"v:01a0eec4-3420-7d50-bfdb-bc7f1701193e:22","workspaceRoot":"/tmp/muse-phase0/repo"}}} +{"dir":"in","msg":{"id":2,"jsonrpc":"2.0","result":{"history":{"items":[{"commandId":"01a0eec4-347d-7a14-a97f-3fd6be129dda","itemId":"38d5182a-7782-429b-83e7-7abbf478b742","kind":"userMessage","recordedAt":"2026-09-29T20:03:51.040894Z","revision":1,"status":"completed","text":"Use todo_write to create two todos: inspect f (in progress) and report (pending). Then use ask_user_question to ask me to choose Blue or Green before proceeding. Do not infer my answer.","turnId":"01a0eec4-347d-7a14-a97f-3fd6be129dda"},{"childSessionId":"ea994a51-a77f-4052-85d7-50112251cccb","failureReason":"main run cancelled","fallbackText":"Reminder child session","generationId":1,"itemId":"b64f0073-af31-4829-82a0-daeb601a87e9","kind":"reminderChild","recordedAt":"2026-09-29T20:05:44.307979Z","reminderAgentId":"skill-reminder","revision":2,"status":"cancelled","taskId":"01a0eec4-3488-7911-aa2c-46f3d9929027","turnId":"01a0eec4-347d-7a14-a97f-3fd6be129dda"},{"args":"{\"todos\":[{\"status\":\"in_progress\",\"text\":\"inspect f\"},{\"status\":\"pending\",\"text\":\"report\"}]}","callId":"call_01a0eec4e0b576aabeae2e104585722e","itemId":"01a0eec4-e2b8-72c3-9a10-5c710d5b666f","kind":"toolCall","recordedAt":"2026-09-29T20:04:35.642482Z","revision":2,"status":"completed","tool":"write_todos","turnId":"01a0eec4-347d-7a14-a97f-3fd6be129dda","visibleOutput":"{\"items\":2,\"ok\":true,\"revision\":1}"},{"args":"{\"questions\":[{\"header\":\"Color\",\"id\":\"color_choice\",\"options\":[{\"label\":\"Blue\"},{\"label\":\"Green\"}],\"question\":\"Which color should I proceed with, Blue or Green?\"}]}","callId":"call_01a0eec5357571379191c94326ef7ab8","itemId":"01a0eec5-3894-7a91-9762-e13cf06c117f","kind":"toolCall","recordedAt":"2026-09-29T20:04:57.624274Z","revision":2,"status":"completed","tool":"request_user_input","turnId":"01a0eec4-347d-7a14-a97f-3fd6be129dda","visibleOutput":"{\"status\":\"answered\",\"answers\":[{\"id\":\"color_choice\",\"selected_label\":\"Blue\"}]}"},{"itemId":"e187c684-c3d9-45e8-bbae-c1507cc9e8d1","kind":"agentMessage","recordedAt":"2026-09-29T20:05:15.287526Z","revision":2,"status":"completed","text":"You chose Blue.\n\nTodos created: inspect f (in progress), report (pending). Awaiting direction to proceed.","turnId":"01a0eec4-347d-7a14-a97f-3fd6be129dda"},{"childSessionId":"cf7f87cb-f82a-441e-b840-b89cfdc03e18","childSessionLogPath":"subagent/cf7f87cb-f82a-441e-b840-b89cfdc03e18/session.jsonl","failureReason":"main run cancelled","fallbackText":"Reminder child session","generationId":1,"itemId":"9da8e5a6-f48f-4cfb-b8ed-79ea0d71f842","kind":"reminderChild","recordedAt":"2026-09-29T20:05:44.308134Z","reminderAgentId":"verify-reminder","revision":2,"status":"cancelled","taskId":"01a0eec5-7d9c-7f61-b098-af5f30813223","turnId":"01a0eec4-347d-7a14-a97f-3fd6be129dda"}],"mode":"inline","snapshot":null},"pendingRequests":[],"session":{"activeTurnId":null,"approvalMode":{"lastCommandId":null,"mode":"onRequest","source":"replay"},"branch":"main","createdAt":"2026-09-29T20:03:50.94472Z","firstUserPrompt":"Use todo_write to create two todos: inspect f (in progress) and report (pending). Then use ask_user_question to ask me to choose Blue or Green before proceeding. Do not infer my answer.","forkedFrom":null,"lastActivityAt":"2026-09-29T20:05:44.308362Z","modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0eec4-3420-7d50-bfdb-bc7f1701193e/session.jsonl","providerId":"meta","sessionId":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","status":"idle","title":"Use todo_write to create two todos: inspect f (in progress) and report (pending). Then use ask_user_question to ask me to choose Blue or Green before proceeding. Do not infer my answer.","turnCount":1,"updatedAt":"2026-09-29T20:08:27.729097Z","workspaceRoot":"/tmp/muse-phase0/repo"},"viewCursor":"v:01a0eec4-3420-7d50-bfdb-bc7f1701193e:22"}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":3,"method":"session/compact","params":{"commandId":"01a0eec8-7193-752f-a80c-afb8c437efab","sessionId":"01a0eec4-3420-7d50-bfdb-bc7f1701193e"}}} +{"dir":"in","msg":{"error":{"code":-32030,"data":{"commandId":"01a0eec8-7193-752f-a80c-afb8c437efab","kind":"commandRejected","reason":"compaction_unavailable","retryable":false},"message":"session/compact command 01a0eec8-7193-752f-a80c-afb8c437efab rejected: compaction_unavailable"},"id":3,"jsonrpc":"2.0"}} +{"dir":"in","msg":{"emittedAtMs":1790712568834,"jsonrpc":"2.0","method":"session/closed","params":{"reason":"hostShutdown","sessionId":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","viewCursor":"v:01a0eec4-3420-7d50-bfdb-bc7f1701193e:22"}}} +{"dir":"in","msg":{"emittedAtMs":1790712568834,"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","status":"notLoaded","viewCursor":"v:01a0eec4-3420-7d50-bfdb-bc7f1701193e:22"}}} +{"dir":"in","msg":{"emittedAtMs":1790712568834,"jsonrpc":"2.0","method":"session/listChanged","params":{"session":{"activeTurnId":null,"branch":"main","createdAt":"2026-09-29T20:03:50.94472Z","firstUserPrompt":"Use todo_write to create two todos: inspect f (in progress) and report (pending). Then use ask_user_question to ask me to choose Blue or Green before proceeding. Do not infer my answer.","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0eec4-3420-7d50-bfdb-bc7f1701193e/session.jsonl","providerId":"meta","sessionId":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","status":"notLoaded","title":"Use todo_write to create two todos: inspect f (in progress) and report (pending). Then use ask_user_question to ask me to choose Blue or Green before proceeding. Do not infer my answer.","turnCount":1,"updatedAt":"2026-09-29T20:08:28.819534Z","workspaceRoot":"/tmp/muse-phase0/repo"}}}} diff --git a/plugins/muse-provider/test/fixtures/phase3-controls.ndjson b/plugins/muse-provider/test/fixtures/phase3-controls.ndjson new file mode 100644 index 0000000000..21d1e56179 --- /dev/null +++ b/plugins/muse-provider/test/fixtures/phase3-controls.ndjson @@ -0,0 +1,51 @@ +{"dir":"out","msg":{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"clientInfo":{"name":"paseo_phase0","version":"0.0.0"},"capabilities":{"experimentalApi":true,"requestedCapabilities":["sessionMcp","sessionListStream"]}}}} +{"dir":"in","msg":{"id":1,"jsonrpc":"2.0","result":{"experimentalApi":true,"grantedCapabilities":["sessionMcp","sessionListStream"],"museHome":"/tmp/muse-phase0/data/muse","platformFamily":"unix","platformOs":"linux","schema":{"fingerprint":"sha256:e0e163db6ccf00dbe68402ce55d6319b3edc33c421f31e9583b587b2de8a118f","version":1},"serverInfo":{"name":"muse","version":"1.4.1"},"sessionDurability":"durable","userAgent":"muse-build/1.4.1 (non-interactive; linux-x86_64; build 35815c253477406321e3f5595195becc25e3907a)"}}} +{"dir":"out","msg":{"jsonrpc":"2.0","method":"initialized","params":{}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":2,"method":"session/start","params":{"commandId":"01a0eec4-3418-78cd-a424-b9e3a0c0eccc","workspaceRoot":"/tmp/muse-phase0/repo","modelId":"meta/muse-spark-1.3","providerId":"meta","approvalMode":"onRequest"}}} +{"dir":"in","msg":{"emittedAtMs":1790712231029,"jsonrpc":"2.0","method":"session/started","params":{"session":{"activeTurnId":null,"approvalMode":{"lastCommandId":null,"mode":"onRequest","source":"startup"},"createdAt":"2026-09-29T20:03:50.947404Z","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0eec4-3420-7d50-bfdb-bc7f1701193e/session.jsonl","providerId":"meta","sessionId":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","status":"idle","turnCount":0,"updatedAt":"2026-09-29T20:03:50.947408Z","workspaceRoot":"/tmp/muse-phase0/repo"}}}} +{"dir":"in","msg":{"id":2,"jsonrpc":"2.0","result":{"session":{"activeTurnId":null,"approvalMode":{"lastCommandId":null,"mode":"onRequest","source":"startup"},"createdAt":"2026-09-29T20:03:50.947404Z","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0eec4-3420-7d50-bfdb-bc7f1701193e/session.jsonl","providerId":"meta","sessionId":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","status":"idle","turnCount":0,"updatedAt":"2026-09-29T20:03:50.947408Z","workspaceRoot":"/tmp/muse-phase0/repo"},"viewCursor":"v:01a0eec4-3420-7d50-bfdb-bc7f1701193e:1"}}} +{"dir":"in","msg":{"emittedAtMs":1790712231037,"jsonrpc":"2.0","method":"session/approvalModeChanged","params":{"clientName":"paseo_phase0","commandId":"01a0eec4-3418-78cd-a424-b9e3a0c0eccc","mode":"onRequest","sessionId":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","source":"approvalReconfigure","sourceRange":{"first":{"id":"88f93a5e-cb0a-486c-b9ee-dd11131d68fc","sequence":5},"last":{"id":"88f93a5e-cb0a-486c-b9ee-dd11131d68fc","sequence":5},"stream":{"id":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","kind":"session"}},"viewCursor":"v:01a0eec4-3420-7d50-bfdb-bc7f1701193e:1"}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":3,"method":"turn/start","params":{"commandId":"01a0eec4-347d-7a14-a97f-3fd6be129dda","sessionId":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","input":[{"type":"text","text":"Use todo_write to create two todos: inspect f (in progress) and report (pending). Then use ask_user_question to ask me to choose Blue or Green before proceeding. Do not infer my answer."}],"reasoningEffort":"low"}}} +{"dir":"in","msg":{"emittedAtMs":1790712231040,"jsonrpc":"2.0","method":"session/branchChanged","params":{"branch":"main","sessionId":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","sourceRange":{"first":{"id":"0b878e43-4718-46d0-97e3-6f8277789f2f","sequence":15},"last":{"id":"0b878e43-4718-46d0-97e3-6f8277789f2f","sequence":15},"stream":{"id":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","kind":"session"}},"vcs":"git","viewCursor":"v:01a0eec4-3420-7d50-bfdb-bc7f1701193e:2","workspaceRoot":"/tmp/muse-phase0/repo"}}} +{"dir":"in","msg":{"id":3,"jsonrpc":"2.0","result":{"commandId":"01a0eec4-347d-7a14-a97f-3fd6be129dda","disposition":"started","startedNewTurn":true,"status":"accepted","turnId":"01a0eec4-347d-7a14-a97f-3fd6be129dda"}}} +{"dir":"in","msg":{"emittedAtMs":1790712231049,"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","status":"running","viewCursor":"v:01a0eec4-3420-7d50-bfdb-bc7f1701193e:3"}}} +{"dir":"in","msg":{"emittedAtMs":1790712231050,"jsonrpc":"2.0","method":"turn/started","params":{"commandId":"01a0eec4-347d-7a14-a97f-3fd6be129dda","sessionId":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","sourceRange":{"first":{"id":"8e541f7e-88da-4639-be2d-3e83a2c56a7b","sequence":17},"last":{"id":"8e541f7e-88da-4639-be2d-3e83a2c56a7b","sequence":17},"stream":{"id":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","kind":"session"}},"turnId":"01a0eec4-347d-7a14-a97f-3fd6be129dda","viewCursor":"v:01a0eec4-3420-7d50-bfdb-bc7f1701193e:3"}}} +{"dir":"in","msg":{"emittedAtMs":1790712231050,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"commandId":"01a0eec4-347d-7a14-a97f-3fd6be129dda","itemId":"38d5182a-7782-429b-83e7-7abbf478b742","kind":"userMessage","recordedAt":"2026-09-29T20:03:51.040894Z","revision":1,"status":"completed","text":"Use todo_write to create two todos: inspect f (in progress) and report (pending). Then use ask_user_question to ask me to choose Blue or Green before proceeding. Do not infer my answer.","turnId":"01a0eec4-347d-7a14-a97f-3fd6be129dda"},"sessionId":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","sourceRange":{"first":{"id":"8e541f7e-88da-4639-be2d-3e83a2c56a7b","sequence":17},"last":{"id":"8e541f7e-88da-4639-be2d-3e83a2c56a7b","sequence":17},"stream":{"id":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","kind":"session"}},"viewCursor":"v:01a0eec4-3420-7d50-bfdb-bc7f1701193e:4"}}} +{"dir":"in","msg":{"emittedAtMs":1790712231050,"jsonrpc":"2.0","method":"item/started","params":{"item":{"childSessionId":"ea994a51-a77f-4052-85d7-50112251cccb","fallbackText":"Reminder child session","generationId":1,"itemId":"b64f0073-af31-4829-82a0-daeb601a87e9","kind":"reminderChild","recordedAt":"2026-09-29T20:03:51.049851Z","reminderAgentId":"skill-reminder","revision":1,"status":"inProgress","taskId":"01a0eec4-3488-7911-aa2c-46f3d9929027","turnId":"01a0eec4-347d-7a14-a97f-3fd6be129dda"},"sessionId":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","sourceRange":{"first":{"id":"b64f0073-af31-4829-82a0-daeb601a87e9","sequence":35},"last":{"id":"b64f0073-af31-4829-82a0-daeb601a87e9","sequence":35},"stream":{"id":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","kind":"session"}},"viewCursor":"v:01a0eec4-3420-7d50-bfdb-bc7f1701193e:5"}}} +{"dir":"in","msg":{"emittedAtMs":1790712231055,"jsonrpc":"2.0","method":"session/listChanged","params":{"session":{"activeTurnId":"01a0eec4-347d-7a14-a97f-3fd6be129dda","createdAt":"2026-09-29T20:03:50.947404Z","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0eec4-3420-7d50-bfdb-bc7f1701193e/session.jsonl","providerId":"meta","sessionId":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","status":"running","turnCount":0,"updatedAt":"2026-09-29T20:03:51.050123Z","workspaceRoot":"/tmp/muse-phase0/repo"}}}} +{"dir":"in","msg":{"emittedAtMs":1790712275639,"jsonrpc":"2.0","method":"session/tokenUsage","params":{"cumulative":{"outputTokens":233,"promptTokens":38162,"totalTokens":38395},"durationMs":44585,"finishReason":"tool_calls","modelId":"meta/muse-spark-1.3","promptTokens":38162,"sessionId":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","sourceRange":{"first":{"id":"5d39893f-b7e3-495d-95e5-cf325a813ee3","sequence":51},"last":{"id":"5d39893f-b7e3-495d-95e5-cf325a813ee3","sequence":51},"stream":{"id":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","kind":"session"}},"totalTokens":38395,"turnId":"01a0eec4-347d-7a14-a97f-3fd6be129dda","usage":{"cacheReadTokens":0,"cacheWriteTokens":0,"cachedTokens":0,"inputTokens":38162,"outputTokens":233,"reasoningTokens":129},"viewCursor":"v:01a0eec4-3420-7d50-bfdb-bc7f1701193e:6"}}} +{"dir":"in","msg":{"emittedAtMs":1790712275641,"jsonrpc":"2.0","method":"item/started","params":{"item":{"args":"{\"todos\":[{\"status\":\"in_progress\",\"text\":\"inspect f\"},{\"status\":\"pending\",\"text\":\"report\"}]}","callId":"call_01a0eec4e0b576aabeae2e104585722e","itemId":"01a0eec4-e2b8-72c3-9a10-5c710d5b666f","kind":"toolCall","recordedAt":"2026-09-29T20:04:35.641535Z","revision":1,"status":"inProgress","tool":"write_todos","turnId":"01a0eec4-347d-7a14-a97f-3fd6be129dda"},"sessionId":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","sourceRange":{"first":{"id":"15effe29-72a7-490e-8724-cece71174573","sequence":53},"last":{"id":"3b4efa42-5403-4efa-8354-f83a360912ce","sequence":58},"stream":{"id":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","kind":"session"}},"viewCursor":"v:01a0eec4-3420-7d50-bfdb-bc7f1701193e:7"}}} +{"dir":"in","msg":{"emittedAtMs":1790712275642,"jsonrpc":"2.0","method":"session/todoListChanged","params":{"items":[{"status":"inProgress","text":"inspect f"},{"status":"pending","text":"report"}],"revision":1,"sessionId":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","sourceRange":{"first":{"id":"bdffd683-b042-4196-a976-5c1435f7162c","sequence":59},"last":{"id":"bdffd683-b042-4196-a976-5c1435f7162c","sequence":59},"stream":{"id":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","kind":"session"}},"sourceTool":"write_todos","viewCursor":"v:01a0eec4-3420-7d50-bfdb-bc7f1701193e:8"}}} +{"dir":"in","msg":{"emittedAtMs":1790712275642,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"args":"{\"todos\":[{\"status\":\"in_progress\",\"text\":\"inspect f\"},{\"status\":\"pending\",\"text\":\"report\"}]}","callId":"call_01a0eec4e0b576aabeae2e104585722e","itemId":"01a0eec4-e2b8-72c3-9a10-5c710d5b666f","kind":"toolCall","recordedAt":"2026-09-29T20:04:35.642482Z","revision":2,"status":"completed","tool":"write_todos","turnId":"01a0eec4-347d-7a14-a97f-3fd6be129dda","visibleOutput":"{\"items\":2,\"ok\":true,\"revision\":1}"},"sessionId":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","sourceRange":{"first":{"id":"15effe29-72a7-490e-8724-cece71174573","sequence":53},"last":{"id":"fe541d1f-abb8-45f7-a2b0-cf7dbdfd69a1","sequence":61},"stream":{"id":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","kind":"session"}},"viewCursor":"v:01a0eec4-3420-7d50-bfdb-bc7f1701193e:9"}}} +{"dir":"in","msg":{"emittedAtMs":1790712297619,"jsonrpc":"2.0","method":"session/tokenUsage","params":{"cumulative":{"outputTokens":402,"promptTokens":76600,"totalTokens":77002},"durationMs":21970,"finishReason":"tool_calls","modelId":"meta/muse-spark-1.3","promptTokens":38438,"sessionId":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","sourceRange":{"first":{"id":"4674331a-f993-458b-b6aa-e36cc160f2d7","sequence":80},"last":{"id":"4674331a-f993-458b-b6aa-e36cc160f2d7","sequence":80},"stream":{"id":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","kind":"session"}},"totalTokens":38607,"turnId":"01a0eec4-347d-7a14-a97f-3fd6be129dda","usage":{"cacheReadTokens":38129,"cacheWriteTokens":0,"cachedTokens":38129,"inputTokens":38438,"outputTokens":169,"reasoningTokens":67},"viewCursor":"v:01a0eec4-3420-7d50-bfdb-bc7f1701193e:10"}}} +{"dir":"in","msg":{"emittedAtMs":1790712297621,"jsonrpc":"2.0","method":"item/started","params":{"item":{"args":"{\"questions\":[{\"header\":\"Color\",\"id\":\"color_choice\",\"options\":[{\"label\":\"Blue\"},{\"label\":\"Green\"}],\"question\":\"Which color should I proceed with, Blue or Green?\"}]}","callId":"call_01a0eec5357571379191c94326ef7ab8","itemId":"01a0eec5-3894-7a91-9762-e13cf06c117f","kind":"toolCall","recordedAt":"2026-09-29T20:04:57.621217Z","revision":1,"status":"inProgress","tool":"request_user_input","turnId":"01a0eec4-347d-7a14-a97f-3fd6be129dda"},"sessionId":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","sourceRange":{"first":{"id":"2ff469cc-5f3f-44dc-a7ac-1f8f8d8e0409","sequence":82},"last":{"id":"78484e0f-7d0b-48be-8b70-606cf6a718aa","sequence":87},"stream":{"id":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","kind":"session"}},"viewCursor":"v:01a0eec4-3420-7d50-bfdb-bc7f1701193e:11"}}} +{"dir":"in","msg":{"emittedAtMs":1790712297621,"jsonrpc":"2.0","method":"session/statusChanged","params":{"attention":["inputPending"],"sessionId":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","status":"running","viewCursor":"v:01a0eec4-3420-7d50-bfdb-bc7f1701193e:12"}}} +{"dir":"in","msg":{"id":1,"jsonrpc":"2.0","method":"userInput/request","params":{"itemId":"01a0eec5-3894-7a91-9762-e13cf06c117f","questions":[{"header":"Color","id":"color_choice","options":[{"label":"Blue"},{"label":"Green"}],"question":"Which color should I proceed with, Blue or Green?","selection":{"mode":"single"}}],"sessionId":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","sourceRange":{"first":{"id":"54c9e4de-cba8-49c1-aced-1721ba3731a8","sequence":88},"last":{"id":"54c9e4de-cba8-49c1-aced-1721ba3731a8","sequence":88},"stream":{"id":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","kind":"session"}},"toolCallId":"call_01a0eec5357571379191c94326ef7ab8","toolName":"request_user_input","turnId":"01a0eec4-347d-7a14-a97f-3fd6be129dda","userInputId":"01a0eec5-3894-7a91-9762-e13cf06c117f","viewCursor":"v:01a0eec4-3420-7d50-bfdb-bc7f1701193e:12"}}} +{"dir":"in","msg":{"emittedAtMs":1790712297622,"jsonrpc":"2.0","method":"userInput/requested","params":{"itemId":"01a0eec5-3894-7a91-9762-e13cf06c117f","questions":[{"header":"Color","id":"color_choice","options":[{"label":"Blue"},{"label":"Green"}],"question":"Which color should I proceed with, Blue or Green?","selection":{"mode":"single"}}],"sessionId":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","sourceRange":{"first":{"id":"54c9e4de-cba8-49c1-aced-1721ba3731a8","sequence":88},"last":{"id":"54c9e4de-cba8-49c1-aced-1721ba3731a8","sequence":88},"stream":{"id":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","kind":"session"}},"toolCallId":"call_01a0eec5357571379191c94326ef7ab8","toolName":"request_user_input","turnId":"01a0eec4-347d-7a14-a97f-3fd6be129dda","userInputId":"01a0eec5-3894-7a91-9762-e13cf06c117f","viewCursor":"v:01a0eec4-3420-7d50-bfdb-bc7f1701193e:12"}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":1,"result":{}}} +{"dir":"in","msg":{"emittedAtMs":1790712297622,"jsonrpc":"2.0","method":"session/listChanged","params":{"session":{"activeTurnId":"01a0eec4-347d-7a14-a97f-3fd6be129dda","attention":["inputPending"],"branch":"main","createdAt":"2026-09-29T20:03:50.94472Z","firstUserPrompt":"Use todo_write to create two todos: inspect f (in progress) and report (pending). Then use ask_user_question to ask me to choose Blue or Green before proceeding. Do not infer my answer.","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0eec4-3420-7d50-bfdb-bc7f1701193e/session.jsonl","providerId":"meta","sessionId":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","status":"running","title":"Use todo_write to create two todos: inspect f (in progress) and report (pending). Then use ask_user_question to ask me to choose Blue or Green before proceeding. Do not infer my answer.","turnCount":0,"updatedAt":"2026-09-29T20:04:57.621488Z","workspaceRoot":"/tmp/muse-phase0/repo"}}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":4,"method":"userInput/answer","params":{"commandId":"01a0eec5-3897-757d-af17-a34de5a1cfe4","sessionId":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","userInputId":"01a0eec5-3894-7a91-9762-e13cf06c117f","answers":[{"questionId":"color_choice","selectedLabel":"Blue"}]}}} +{"dir":"in","msg":{"id":4,"jsonrpc":"2.0","result":{"commandId":"01a0eec5-3897-757d-af17-a34de5a1cfe4","status":"accepted","userInputId":"01a0eec5-3894-7a91-9762-e13cf06c117f"}}} +{"dir":"in","msg":{"emittedAtMs":1790712297624,"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","status":"running","viewCursor":"v:01a0eec4-3420-7d50-bfdb-bc7f1701193e:13"}}} +{"dir":"in","msg":{"emittedAtMs":1790712297624,"jsonrpc":"2.0","method":"userInput/settled","params":{"answers":[{"questionId":"color_choice","selectedLabel":"Blue"}],"clarification":null,"decidedByCommandId":"01a0eec5-3897-757d-af17-a34de5a1cfe4","outcome":"answered","reason":null,"sessionId":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","sourceRange":{"first":{"id":"909310e9-7253-4b4c-963e-2f46628f1aae","sequence":91},"last":{"id":"909310e9-7253-4b4c-963e-2f46628f1aae","sequence":91},"stream":{"id":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","kind":"session"}},"userInputId":"01a0eec5-3894-7a91-9762-e13cf06c117f","viewCursor":"v:01a0eec4-3420-7d50-bfdb-bc7f1701193e:13"}}} +{"dir":"in","msg":{"emittedAtMs":1790712297624,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"args":"{\"questions\":[{\"header\":\"Color\",\"id\":\"color_choice\",\"options\":[{\"label\":\"Blue\"},{\"label\":\"Green\"}],\"question\":\"Which color should I proceed with, Blue or Green?\"}]}","callId":"call_01a0eec5357571379191c94326ef7ab8","itemId":"01a0eec5-3894-7a91-9762-e13cf06c117f","kind":"toolCall","recordedAt":"2026-09-29T20:04:57.624274Z","revision":2,"status":"completed","tool":"request_user_input","turnId":"01a0eec4-347d-7a14-a97f-3fd6be129dda","visibleOutput":"{\"status\":\"answered\",\"answers\":[{\"id\":\"color_choice\",\"selected_label\":\"Blue\"}]}"},"sessionId":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","sourceRange":{"first":{"id":"2ff469cc-5f3f-44dc-a7ac-1f8f8d8e0409","sequence":82},"last":{"id":"b7264ba7-6895-475a-a4b8-56bed8767e35","sequence":93},"stream":{"id":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","kind":"session"}},"viewCursor":"v:01a0eec4-3420-7d50-bfdb-bc7f1701193e:14"}}} +{"dir":"in","msg":{"emittedAtMs":1790712297630,"jsonrpc":"2.0","method":"session/listChanged","params":{"session":{"activeTurnId":"01a0eec4-347d-7a14-a97f-3fd6be129dda","branch":"main","createdAt":"2026-09-29T20:03:50.94472Z","firstUserPrompt":"Use todo_write to create two todos: inspect f (in progress) and report (pending). Then use ask_user_question to ask me to choose Blue or Green before proceeding. Do not infer my answer.","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0eec4-3420-7d50-bfdb-bc7f1701193e/session.jsonl","providerId":"meta","sessionId":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","status":"running","title":"Use todo_write to create two todos: inspect f (in progress) and report (pending). Then use ask_user_question to ask me to choose Blue or Green before proceeding. Do not infer my answer.","turnCount":0,"updatedAt":"2026-09-29T20:04:57.624274Z","workspaceRoot":"/tmp/muse-phase0/repo"}}}} +{"dir":"in","msg":{"emittedAtMs":1790712315201,"jsonrpc":"2.0","method":"item/started","params":{"item":{"itemId":"e187c684-c3d9-45e8-bbae-c1507cc9e8d1","kind":"agentMessage","revision":1,"status":"inProgress","text":"","turnId":"01a0eec4-347d-7a14-a97f-3fd6be129dda"},"sessionId":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","viewCursor":"v:01a0eec4-3420-7d50-bfdb-bc7f1701193e:14.1"}}} +{"dir":"in","msg":{"emittedAtMs":1790712315201,"jsonrpc":"2.0","method":"item/delta","params":{"delta":"You","field":"text","itemId":"e187c684-c3d9-45e8-bbae-c1507cc9e8d1","sessionId":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","viewCursor":"v:01a0eec4-3420-7d50-bfdb-bc7f1701193e:14.2"}}} +{"dir":"in","msg":{"emittedAtMs":1790712315201,"jsonrpc":"2.0","method":"item/delta","params":{"delta":" chose Blue.\n\nTodos created: inspect f (","field":"text","itemId":"e187c684-c3d9-45e8-bbae-c1507cc9e8d1","sessionId":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","viewCursor":"v:01a0eec4-3420-7d50-bfdb-bc7f1701193e:14.3"}}} +{"dir":"in","msg":{"emittedAtMs":1790712315201,"jsonrpc":"2.0","method":"item/delta","params":{"delta":"in progress), report (pending). Awaiting direction","field":"text","itemId":"e187c684-c3d9-45e8-bbae-c1507cc9e8d1","sessionId":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","viewCursor":"v:01a0eec4-3420-7d50-bfdb-bc7f1701193e:14.4"}}} +{"dir":"in","msg":{"emittedAtMs":1790712315201,"jsonrpc":"2.0","method":"item/delta","params":{"delta":" to proceed.","field":"text","itemId":"e187c684-c3d9-45e8-bbae-c1507cc9e8d1","sessionId":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","viewCursor":"v:01a0eec4-3420-7d50-bfdb-bc7f1701193e:14.5"}}} +{"dir":"in","msg":{"emittedAtMs":1790712315287,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"itemId":"e187c684-c3d9-45e8-bbae-c1507cc9e8d1","kind":"agentMessage","recordedAt":"2026-09-29T20:05:15.287526Z","revision":2,"status":"completed","text":"You chose Blue.\n\nTodos created: inspect f (in progress), report (pending). Awaiting direction to proceed.","turnId":"01a0eec4-347d-7a14-a97f-3fd6be129dda"},"sessionId":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","sourceRange":{"first":{"id":"6834f18d-2fc1-4a06-8a21-bfb486bd0338","sequence":110},"last":{"id":"6834f18d-2fc1-4a06-8a21-bfb486bd0338","sequence":110},"stream":{"id":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","kind":"session"}},"viewCursor":"v:01a0eec4-3420-7d50-bfdb-bc7f1701193e:15"}}} +{"dir":"in","msg":{"emittedAtMs":1790712315292,"jsonrpc":"2.0","method":"session/tokenUsage","params":{"cumulative":{"outputTokens":525,"promptTokens":115255,"totalTokens":115780},"durationMs":17661,"modelId":"meta/muse-spark-1.3","promptTokens":38655,"sessionId":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","sourceRange":{"first":{"id":"a5633a10-4fb0-4873-b47d-4d558bd3c4e5","sequence":113},"last":{"id":"a5633a10-4fb0-4873-b47d-4d558bd3c4e5","sequence":113},"stream":{"id":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","kind":"session"}},"totalTokens":38778,"turnId":"01a0eec4-347d-7a14-a97f-3fd6be129dda","usage":{"cacheReadTokens":38385,"cacheWriteTokens":0,"cachedTokens":38385,"inputTokens":38655,"outputTokens":123,"reasoningTokens":90},"viewCursor":"v:01a0eec4-3420-7d50-bfdb-bc7f1701193e:16"}}} +{"dir":"in","msg":{"emittedAtMs":1790712315294,"jsonrpc":"2.0","method":"item/started","params":{"item":{"childSessionId":"cf7f87cb-f82a-441e-b840-b89cfdc03e18","childSessionLogPath":"subagent/cf7f87cb-f82a-441e-b840-b89cfdc03e18/session.jsonl","fallbackText":"Reminder child session","generationId":1,"itemId":"9da8e5a6-f48f-4cfb-b8ed-79ea0d71f842","kind":"reminderChild","recordedAt":"2026-09-29T20:05:15.294057Z","reminderAgentId":"verify-reminder","revision":1,"status":"inProgress","taskId":"01a0eec5-7d9c-7f61-b098-af5f30813223","turnId":"01a0eec4-347d-7a14-a97f-3fd6be129dda"},"sessionId":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","sourceRange":{"first":{"id":"9da8e5a6-f48f-4cfb-b8ed-79ea0d71f842","sequence":120},"last":{"id":"9da8e5a6-f48f-4cfb-b8ed-79ea0d71f842","sequence":120},"stream":{"id":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","kind":"session"}},"viewCursor":"v:01a0eec4-3420-7d50-bfdb-bc7f1701193e:17"}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":5,"method":"view/page","params":{"sessionId":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","limit":1000}}} +{"dir":"in","msg":{"id":5,"jsonrpc":"2.0","result":{"events":[{"method":"session/approvalModeChanged","params":{"clientName":"paseo_phase0","commandId":"01a0eec4-3418-78cd-a424-b9e3a0c0eccc","mode":"onRequest","sessionId":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","source":"approvalReconfigure","sourceRange":{"first":{"id":"88f93a5e-cb0a-486c-b9ee-dd11131d68fc","sequence":5},"last":{"id":"88f93a5e-cb0a-486c-b9ee-dd11131d68fc","sequence":5},"stream":{"id":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","kind":"session"}},"viewCursor":"v:01a0eec4-3420-7d50-bfdb-bc7f1701193e:1"}},{"method":"session/branchChanged","params":{"branch":"main","sessionId":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","sourceRange":{"first":{"id":"0b878e43-4718-46d0-97e3-6f8277789f2f","sequence":15},"last":{"id":"0b878e43-4718-46d0-97e3-6f8277789f2f","sequence":15},"stream":{"id":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","kind":"session"}},"vcs":"git","viewCursor":"v:01a0eec4-3420-7d50-bfdb-bc7f1701193e:2","workspaceRoot":"/tmp/muse-phase0/repo"}},{"method":"turn/started","params":{"commandId":"01a0eec4-347d-7a14-a97f-3fd6be129dda","sessionId":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","sourceRange":{"first":{"id":"8e541f7e-88da-4639-be2d-3e83a2c56a7b","sequence":17},"last":{"id":"8e541f7e-88da-4639-be2d-3e83a2c56a7b","sequence":17},"stream":{"id":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","kind":"session"}},"turnId":"01a0eec4-347d-7a14-a97f-3fd6be129dda","viewCursor":"v:01a0eec4-3420-7d50-bfdb-bc7f1701193e:3"}},{"method":"item/completed","params":{"item":{"commandId":"01a0eec4-347d-7a14-a97f-3fd6be129dda","itemId":"38d5182a-7782-429b-83e7-7abbf478b742","kind":"userMessage","recordedAt":"2026-09-29T20:03:51.040894Z","revision":1,"status":"completed","text":"Use todo_write to create two todos: inspect f (in progress) and report (pending). Then use ask_user_question to ask me to choose Blue or Green before proceeding. Do not infer my answer.","turnId":"01a0eec4-347d-7a14-a97f-3fd6be129dda"},"sessionId":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","sourceRange":{"first":{"id":"8e541f7e-88da-4639-be2d-3e83a2c56a7b","sequence":17},"last":{"id":"8e541f7e-88da-4639-be2d-3e83a2c56a7b","sequence":17},"stream":{"id":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","kind":"session"}},"viewCursor":"v:01a0eec4-3420-7d50-bfdb-bc7f1701193e:4"}},{"method":"item/started","params":{"item":{"childSessionId":"ea994a51-a77f-4052-85d7-50112251cccb","fallbackText":"Reminder child session","generationId":1,"itemId":"b64f0073-af31-4829-82a0-daeb601a87e9","kind":"reminderChild","recordedAt":"2026-09-29T20:03:51.049851Z","reminderAgentId":"skill-reminder","revision":1,"status":"inProgress","taskId":"01a0eec4-3488-7911-aa2c-46f3d9929027","turnId":"01a0eec4-347d-7a14-a97f-3fd6be129dda"},"sessionId":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","sourceRange":{"first":{"id":"b64f0073-af31-4829-82a0-daeb601a87e9","sequence":35},"last":{"id":"b64f0073-af31-4829-82a0-daeb601a87e9","sequence":35},"stream":{"id":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","kind":"session"}},"viewCursor":"v:01a0eec4-3420-7d50-bfdb-bc7f1701193e:5"}},{"method":"session/tokenUsage","params":{"cumulative":{"outputTokens":233,"promptTokens":38162,"totalTokens":38395},"durationMs":44585,"finishReason":"tool_calls","modelId":"meta/muse-spark-1.3","promptTokens":38162,"sessionId":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","sourceRange":{"first":{"id":"5d39893f-b7e3-495d-95e5-cf325a813ee3","sequence":51},"last":{"id":"5d39893f-b7e3-495d-95e5-cf325a813ee3","sequence":51},"stream":{"id":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","kind":"session"}},"totalTokens":38395,"turnId":"01a0eec4-347d-7a14-a97f-3fd6be129dda","usage":{"cacheReadTokens":0,"cacheWriteTokens":0,"cachedTokens":0,"inputTokens":38162,"outputTokens":233,"reasoningTokens":129},"viewCursor":"v:01a0eec4-3420-7d50-bfdb-bc7f1701193e:6"}},{"method":"item/started","params":{"item":{"args":"{\"todos\":[{\"status\":\"in_progress\",\"text\":\"inspect f\"},{\"status\":\"pending\",\"text\":\"report\"}]}","callId":"call_01a0eec4e0b576aabeae2e104585722e","itemId":"01a0eec4-e2b8-72c3-9a10-5c710d5b666f","kind":"toolCall","recordedAt":"2026-09-29T20:04:35.641535Z","revision":1,"status":"inProgress","tool":"write_todos","turnId":"01a0eec4-347d-7a14-a97f-3fd6be129dda"},"sessionId":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","sourceRange":{"first":{"id":"15effe29-72a7-490e-8724-cece71174573","sequence":53},"last":{"id":"3b4efa42-5403-4efa-8354-f83a360912ce","sequence":58},"stream":{"id":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","kind":"session"}},"viewCursor":"v:01a0eec4-3420-7d50-bfdb-bc7f1701193e:7"}},{"method":"session/todoListChanged","params":{"items":[{"status":"inProgress","text":"inspect f"},{"status":"pending","text":"report"}],"revision":1,"sessionId":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","sourceRange":{"first":{"id":"bdffd683-b042-4196-a976-5c1435f7162c","sequence":59},"last":{"id":"bdffd683-b042-4196-a976-5c1435f7162c","sequence":59},"stream":{"id":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","kind":"session"}},"sourceTool":"write_todos","viewCursor":"v:01a0eec4-3420-7d50-bfdb-bc7f1701193e:8"}},{"method":"item/completed","params":{"item":{"args":"{\"todos\":[{\"status\":\"in_progress\",\"text\":\"inspect f\"},{\"status\":\"pending\",\"text\":\"report\"}]}","callId":"call_01a0eec4e0b576aabeae2e104585722e","itemId":"01a0eec4-e2b8-72c3-9a10-5c710d5b666f","kind":"toolCall","recordedAt":"2026-09-29T20:04:35.642482Z","revision":2,"status":"completed","tool":"write_todos","turnId":"01a0eec4-347d-7a14-a97f-3fd6be129dda","visibleOutput":"{\"items\":2,\"ok\":true,\"revision\":1}"},"sessionId":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","sourceRange":{"first":{"id":"15effe29-72a7-490e-8724-cece71174573","sequence":53},"last":{"id":"fe541d1f-abb8-45f7-a2b0-cf7dbdfd69a1","sequence":61},"stream":{"id":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","kind":"session"}},"viewCursor":"v:01a0eec4-3420-7d50-bfdb-bc7f1701193e:9"}},{"method":"session/tokenUsage","params":{"cumulative":{"outputTokens":402,"promptTokens":76600,"totalTokens":77002},"durationMs":21970,"finishReason":"tool_calls","modelId":"meta/muse-spark-1.3","promptTokens":38438,"sessionId":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","sourceRange":{"first":{"id":"4674331a-f993-458b-b6aa-e36cc160f2d7","sequence":80},"last":{"id":"4674331a-f993-458b-b6aa-e36cc160f2d7","sequence":80},"stream":{"id":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","kind":"session"}},"totalTokens":38607,"turnId":"01a0eec4-347d-7a14-a97f-3fd6be129dda","usage":{"cacheReadTokens":38129,"cacheWriteTokens":0,"cachedTokens":38129,"inputTokens":38438,"outputTokens":169,"reasoningTokens":67},"viewCursor":"v:01a0eec4-3420-7d50-bfdb-bc7f1701193e:10"}},{"method":"item/started","params":{"item":{"args":"{\"questions\":[{\"header\":\"Color\",\"id\":\"color_choice\",\"options\":[{\"label\":\"Blue\"},{\"label\":\"Green\"}],\"question\":\"Which color should I proceed with, Blue or Green?\"}]}","callId":"call_01a0eec5357571379191c94326ef7ab8","itemId":"01a0eec5-3894-7a91-9762-e13cf06c117f","kind":"toolCall","recordedAt":"2026-09-29T20:04:57.621217Z","revision":1,"status":"inProgress","tool":"request_user_input","turnId":"01a0eec4-347d-7a14-a97f-3fd6be129dda"},"sessionId":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","sourceRange":{"first":{"id":"2ff469cc-5f3f-44dc-a7ac-1f8f8d8e0409","sequence":82},"last":{"id":"78484e0f-7d0b-48be-8b70-606cf6a718aa","sequence":87},"stream":{"id":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","kind":"session"}},"viewCursor":"v:01a0eec4-3420-7d50-bfdb-bc7f1701193e:11"}},{"method":"userInput/requested","params":{"itemId":"01a0eec5-3894-7a91-9762-e13cf06c117f","questions":[{"header":"Color","id":"color_choice","options":[{"label":"Blue"},{"label":"Green"}],"question":"Which color should I proceed with, Blue or Green?","selection":{"mode":"single"}}],"sessionId":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","sourceRange":{"first":{"id":"54c9e4de-cba8-49c1-aced-1721ba3731a8","sequence":88},"last":{"id":"54c9e4de-cba8-49c1-aced-1721ba3731a8","sequence":88},"stream":{"id":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","kind":"session"}},"toolCallId":"call_01a0eec5357571379191c94326ef7ab8","toolName":"request_user_input","turnId":"01a0eec4-347d-7a14-a97f-3fd6be129dda","userInputId":"01a0eec5-3894-7a91-9762-e13cf06c117f","viewCursor":"v:01a0eec4-3420-7d50-bfdb-bc7f1701193e:12"}},{"method":"userInput/settled","params":{"answers":[{"questionId":"color_choice","selectedLabel":"Blue"}],"clarification":null,"decidedByCommandId":"01a0eec5-3897-757d-af17-a34de5a1cfe4","outcome":"answered","reason":null,"sessionId":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","sourceRange":{"first":{"id":"909310e9-7253-4b4c-963e-2f46628f1aae","sequence":91},"last":{"id":"909310e9-7253-4b4c-963e-2f46628f1aae","sequence":91},"stream":{"id":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","kind":"session"}},"userInputId":"01a0eec5-3894-7a91-9762-e13cf06c117f","viewCursor":"v:01a0eec4-3420-7d50-bfdb-bc7f1701193e:13"}},{"method":"item/completed","params":{"item":{"args":"{\"questions\":[{\"header\":\"Color\",\"id\":\"color_choice\",\"options\":[{\"label\":\"Blue\"},{\"label\":\"Green\"}],\"question\":\"Which color should I proceed with, Blue or Green?\"}]}","callId":"call_01a0eec5357571379191c94326ef7ab8","itemId":"01a0eec5-3894-7a91-9762-e13cf06c117f","kind":"toolCall","recordedAt":"2026-09-29T20:04:57.624274Z","revision":2,"status":"completed","tool":"request_user_input","turnId":"01a0eec4-347d-7a14-a97f-3fd6be129dda","visibleOutput":"{\"status\":\"answered\",\"answers\":[{\"id\":\"color_choice\",\"selected_label\":\"Blue\"}]}"},"sessionId":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","sourceRange":{"first":{"id":"2ff469cc-5f3f-44dc-a7ac-1f8f8d8e0409","sequence":82},"last":{"id":"b7264ba7-6895-475a-a4b8-56bed8767e35","sequence":93},"stream":{"id":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","kind":"session"}},"viewCursor":"v:01a0eec4-3420-7d50-bfdb-bc7f1701193e:14"}},{"method":"item/completed","params":{"item":{"itemId":"e187c684-c3d9-45e8-bbae-c1507cc9e8d1","kind":"agentMessage","recordedAt":"2026-09-29T20:05:15.287526Z","revision":2,"status":"completed","text":"You chose Blue.\n\nTodos created: inspect f (in progress), report (pending). Awaiting direction to proceed.","turnId":"01a0eec4-347d-7a14-a97f-3fd6be129dda"},"sessionId":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","sourceRange":{"first":{"id":"6834f18d-2fc1-4a06-8a21-bfb486bd0338","sequence":110},"last":{"id":"6834f18d-2fc1-4a06-8a21-bfb486bd0338","sequence":110},"stream":{"id":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","kind":"session"}},"viewCursor":"v:01a0eec4-3420-7d50-bfdb-bc7f1701193e:15"}},{"method":"session/tokenUsage","params":{"cumulative":{"outputTokens":525,"promptTokens":115255,"totalTokens":115780},"durationMs":17661,"modelId":"meta/muse-spark-1.3","promptTokens":38655,"sessionId":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","sourceRange":{"first":{"id":"a5633a10-4fb0-4873-b47d-4d558bd3c4e5","sequence":113},"last":{"id":"a5633a10-4fb0-4873-b47d-4d558bd3c4e5","sequence":113},"stream":{"id":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","kind":"session"}},"totalTokens":38778,"turnId":"01a0eec4-347d-7a14-a97f-3fd6be129dda","usage":{"cacheReadTokens":38385,"cacheWriteTokens":0,"cachedTokens":38385,"inputTokens":38655,"outputTokens":123,"reasoningTokens":90},"viewCursor":"v:01a0eec4-3420-7d50-bfdb-bc7f1701193e:16"}},{"method":"item/started","params":{"item":{"childSessionId":"cf7f87cb-f82a-441e-b840-b89cfdc03e18","childSessionLogPath":"subagent/cf7f87cb-f82a-441e-b840-b89cfdc03e18/session.jsonl","fallbackText":"Reminder child session","generationId":1,"itemId":"9da8e5a6-f48f-4cfb-b8ed-79ea0d71f842","kind":"reminderChild","recordedAt":"2026-09-29T20:05:15.294057Z","reminderAgentId":"verify-reminder","revision":1,"status":"inProgress","taskId":"01a0eec5-7d9c-7f61-b098-af5f30813223","turnId":"01a0eec4-347d-7a14-a97f-3fd6be129dda"},"sessionId":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","sourceRange":{"first":{"id":"9da8e5a6-f48f-4cfb-b8ed-79ea0d71f842","sequence":120},"last":{"id":"9da8e5a6-f48f-4cfb-b8ed-79ea0d71f842","sequence":120},"stream":{"id":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","kind":"session"}},"viewCursor":"v:01a0eec4-3420-7d50-bfdb-bc7f1701193e:17"}}],"nextCursor":null}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":6,"method":"session/read","params":{"sessionId":"01a0eec4-3420-7d50-bfdb-bc7f1701193e"}}} +{"dir":"in","msg":{"id":6,"jsonrpc":"2.0","result":{"history":{"items":null,"mode":"none","noneReason":"excluded","snapshot":null},"pendingRequests":[],"session":{"activeTurnId":"01a0eec4-347d-7a14-a97f-3fd6be129dda","approvalMode":{"lastCommandId":null,"mode":"onRequest","source":"startup"},"branch":"main","createdAt":"2026-09-29T20:03:50.94472Z","firstUserPrompt":"Use todo_write to create two todos: inspect f (in progress) and report (pending). Then use ask_user_question to ask me to choose Blue or Green before proceeding. Do not infer my answer.","forkedFrom":null,"lastActivityAt":"2026-09-29T20:05:15.294057Z","modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0eec4-3420-7d50-bfdb-bc7f1701193e/session.jsonl","providerId":"meta","sessionId":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","status":"running","title":"Use todo_write to create two todos: inspect f (in progress) and report (pending). Then use ask_user_question to ask me to choose Blue or Green before proceeding. Do not infer my answer.","turnCount":0,"updatedAt":"2026-09-29T20:05:15.294321Z","workspaceRoot":"/tmp/muse-phase0/repo"},"viewCursor":"v:01a0eec4-3420-7d50-bfdb-bc7f1701193e:17"}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":7,"method":"session/compact","params":{"commandId":"01a0eec5-e331-73ef-ab77-595e061742b7","sessionId":"01a0eec4-3420-7d50-bfdb-bc7f1701193e"}}} +{"dir":"in","msg":{"error":{"code":-32030,"data":{"commandId":"01a0eec5-e331-73ef-ab77-595e061742b7","kind":"commandRejected","reason":"run_active","retryable":false},"message":"session/compact command 01a0eec5-e331-73ef-ab77-595e061742b7 rejected: run_active"},"id":7,"jsonrpc":"2.0"}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":8,"method":"usage/read","params":{}}} +{"dir":"in","msg":{"id":8,"jsonrpc":"2.0","result":{}}} +{"dir":"in","msg":{"emittedAtMs":1790712344524,"jsonrpc":"2.0","method":"session/closed","params":{"reason":"hostShutdown","sessionId":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","viewCursor":"v:01a0eec4-3420-7d50-bfdb-bc7f1701193e:21"}}} +{"dir":"in","msg":{"emittedAtMs":1790712344524,"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","status":"notLoaded","viewCursor":"v:01a0eec4-3420-7d50-bfdb-bc7f1701193e:21"}}} +{"dir":"in","msg":{"emittedAtMs":1790712344524,"jsonrpc":"2.0","method":"session/listChanged","params":{"session":{"activeTurnId":null,"branch":"main","createdAt":"2026-09-29T20:03:50.94472Z","firstUserPrompt":"Use todo_write to create two todos: inspect f (in progress) and report (pending). Then use ask_user_question to ask me to choose Blue or Green before proceeding. Do not infer my answer.","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0eec4-3420-7d50-bfdb-bc7f1701193e/session.jsonl","providerId":"meta","sessionId":"01a0eec4-3420-7d50-bfdb-bc7f1701193e","status":"notLoaded","title":"Use todo_write to create two todos: inspect f (in progress) and report (pending). Then use ask_user_question to ask me to choose Blue or Green before proceeding. Do not infer my answer.","turnCount":0,"updatedAt":"2026-09-29T20:05:41.304053Z","workspaceRoot":"/tmp/muse-phase0/repo"}}}} diff --git a/plugins/muse-provider/test/fixtures/phase3-default-workflow.ndjson b/plugins/muse-provider/test/fixtures/phase3-default-workflow.ndjson new file mode 100644 index 0000000000..751619557f --- /dev/null +++ b/plugins/muse-provider/test/fixtures/phase3-default-workflow.ndjson @@ -0,0 +1,7 @@ +{"dir":"out","msg":{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"clientInfo":{"name":"paseo_phase0","version":"0.0.0"},"capabilities":{"experimentalApi":true,"requestedCapabilities":["sessionMcp","sessionListStream"]}}}} +{"dir":"in","msg":{"id":1,"jsonrpc":"2.0","result":{"experimentalApi":true,"grantedCapabilities":["sessionMcp","sessionListStream"],"museHome":"/tmp/muse-parity/data/muse","platformFamily":"unix","platformOs":"linux","schema":{"fingerprint":"sha256:e0e163db6ccf00dbe68402ce55d6319b3edc33c421f31e9583b587b2de8a118f","version":1},"serverInfo":{"name":"muse","version":"1.4.1"},"sessionDurability":"durable","userAgent":"muse-build/1.4.1 (non-interactive; linux-x86_64; build 35815c253477406321e3f5595195becc25e3907a)"}}} +{"dir":"out","msg":{"jsonrpc":"2.0","method":"initialized","params":{}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":2,"method":"session/read","params":{"sessionId":"01a0eed3-932b-73e3-af13-d270ac13c13e"}}} +{"dir":"in","msg":{"id":2,"jsonrpc":"2.0","result":{"history":{"items":null,"mode":"none","noneReason":"excluded","snapshot":null},"pendingRequests":[],"session":{"activeTurnId":null,"approvalMode":{"lastCommandId":null,"mode":"onRequest","source":"replay"},"branch":"main","createdAt":"2026-09-29T20:20:38.31556Z","firstUserPrompt":"Reply exactly MUSE_PARITY_READY. Do not use tools.","forkedFrom":null,"lastActivityAt":"2026-09-29T20:29:35.688075Z","modelId":"meta/muse-spark-1.3","path":"/tmp/muse-parity/data/muse/sessions/2026/09/29/01a0eed3-932b-73e3-af13-d270ac13c13e/session.jsonl","providerId":"meta","sessionId":"01a0eed3-932b-73e3-af13-d270ac13c13e","status":"notLoaded","title":"Reply exactly MUSE_PARITY_READY. Do not use tools.","turnCount":4,"updatedAt":"2026-09-29T20:29:35.688075Z","workspaceRoot":"/tmp/muse-parity/repo"},"viewCursor":"v:01a0eed3-932b-73e3-af13-d270ac13c13e:55"}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":3,"method":"view/page","params":{"sessionId":"01a0eed3-932b-73e3-af13-d270ac13c13e","limit":1000}}} +{"dir":"in","msg":{"id":3,"jsonrpc":"2.0","result":{"events":[{"method":"session/approvalModeChanged","params":{"clientName":"paseo","commandId":"01a0eed3-931c-7e09-940b-a18c476535fc","mode":"onRequest","sessionId":"01a0eed3-932b-73e3-af13-d270ac13c13e","source":"approvalReconfigure","sourceRange":{"first":{"id":"e5f590d4-1e22-4de3-87e2-2fce1890b73e","sequence":5},"last":{"id":"e5f590d4-1e22-4de3-87e2-2fce1890b73e","sequence":5},"stream":{"id":"01a0eed3-932b-73e3-af13-d270ac13c13e","kind":"session"}},"viewCursor":"v:01a0eed3-932b-73e3-af13-d270ac13c13e:1"}},{"method":"session/branchChanged","params":{"branch":"main","sessionId":"01a0eed3-932b-73e3-af13-d270ac13c13e","sourceRange":{"first":{"id":"3e7b25da-ae59-4948-bf34-dbcc64cc6ed4","sequence":27},"last":{"id":"3e7b25da-ae59-4948-bf34-dbcc64cc6ed4","sequence":27},"stream":{"id":"01a0eed3-932b-73e3-af13-d270ac13c13e","kind":"session"}},"vcs":"git","viewCursor":"v:01a0eed3-932b-73e3-af13-d270ac13c13e:2","workspaceRoot":"/tmp/muse-parity/repo"}},{"method":"turn/started","params":{"commandId":"01a0eed3-9490-7d1f-9d49-37a21c167827","sessionId":"01a0eed3-932b-73e3-af13-d270ac13c13e","sourceRange":{"first":{"id":"621ad9a3-090f-4de3-bcc1-04bde2eb4275","sequence":29},"last":{"id":"621ad9a3-090f-4de3-bcc1-04bde2eb4275","sequence":29},"stream":{"id":"01a0eed3-932b-73e3-af13-d270ac13c13e","kind":"session"}},"turnId":"01a0eed3-9490-7d1f-9d49-37a21c167827","viewCursor":"v:01a0eed3-932b-73e3-af13-d270ac13c13e:3"}},{"method":"item/completed","params":{"item":{"commandId":"01a0eed3-9490-7d1f-9d49-37a21c167827","itemId":"13033301-7a88-4975-b649-6b625d06f6c2","kind":"userMessage","recordedAt":"2026-09-29T20:20:38.67983Z","revision":1,"status":"completed","text":"Reply exactly MUSE_PARITY_READY. Do not use tools.","turnId":"01a0eed3-9490-7d1f-9d49-37a21c167827"},"sessionId":"01a0eed3-932b-73e3-af13-d270ac13c13e","sourceRange":{"first":{"id":"621ad9a3-090f-4de3-bcc1-04bde2eb4275","sequence":29},"last":{"id":"621ad9a3-090f-4de3-bcc1-04bde2eb4275","sequence":29},"stream":{"id":"01a0eed3-932b-73e3-af13-d270ac13c13e","kind":"session"}},"viewCursor":"v:01a0eed3-932b-73e3-af13-d270ac13c13e:4"}},{"method":"item/started","params":{"item":{"childSessionId":"cec38684-5da4-4884-a207-ef7834c0c389","fallbackText":"Reminder child session","generationId":1,"itemId":"5e203ac7-881e-4731-b6dc-a887c5763cce","kind":"reminderChild","recordedAt":"2026-09-29T20:20:38.697965Z","reminderAgentId":"skill-reminder","revision":1,"status":"inProgress","taskId":"01a0eed3-94a8-7051-a7b4-675143c10378","turnId":"01a0eed3-9490-7d1f-9d49-37a21c167827"},"sessionId":"01a0eed3-932b-73e3-af13-d270ac13c13e","sourceRange":{"first":{"id":"5e203ac7-881e-4731-b6dc-a887c5763cce","sequence":47},"last":{"id":"5e203ac7-881e-4731-b6dc-a887c5763cce","sequence":47},"stream":{"id":"01a0eed3-932b-73e3-af13-d270ac13c13e","kind":"session"}},"viewCursor":"v:01a0eed3-932b-73e3-af13-d270ac13c13e:5"}},{"method":"item/completed","params":{"item":{"itemId":"c77e54d5-dd15-40a3-a5e0-0d978d17f3c2","kind":"agentMessage","recordedAt":"2026-09-29T20:20:40.835405Z","revision":2,"status":"completed","text":"MUSE_PARITY_READY","turnId":"01a0eed3-9490-7d1f-9d49-37a21c167827"},"sessionId":"01a0eed3-932b-73e3-af13-d270ac13c13e","sourceRange":{"first":{"id":"96c261f4-c700-4559-86c8-9b550158256c","sequence":60},"last":{"id":"96c261f4-c700-4559-86c8-9b550158256c","sequence":60},"stream":{"id":"01a0eed3-932b-73e3-af13-d270ac13c13e","kind":"session"}},"viewCursor":"v:01a0eed3-932b-73e3-af13-d270ac13c13e:6"}},{"method":"session/tokenUsage","params":{"cumulative":{"outputTokens":28,"promptTokens":42376,"totalTokens":42404},"durationMs":2138,"modelId":"meta/muse-spark-1.3","promptTokens":42376,"sessionId":"01a0eed3-932b-73e3-af13-d270ac13c13e","sourceRange":{"first":{"id":"6a7ec8a7-2079-4ae2-a891-fcb2f8a07ec8","sequence":63},"last":{"id":"6a7ec8a7-2079-4ae2-a891-fcb2f8a07ec8","sequence":63},"stream":{"id":"01a0eed3-932b-73e3-af13-d270ac13c13e","kind":"session"}},"totalTokens":42404,"turnId":"01a0eed3-9490-7d1f-9d49-37a21c167827","usage":{"cacheReadTokens":0,"cacheWriteTokens":0,"cachedTokens":0,"inputTokens":42376,"outputTokens":28,"reasoningTokens":13},"viewCursor":"v:01a0eed3-932b-73e3-af13-d270ac13c13e:7"}},{"method":"item/started","params":{"item":{"childSessionId":"3d09af71-0bcc-4b94-ba8f-5acf92f259fe","fallbackText":"Reminder child session","generationId":1,"itemId":"74a8a721-4c79-4f69-8ff6-45ed99f86cad","kind":"reminderChild","recordedAt":"2026-09-29T20:20:40.843309Z","reminderAgentId":"verify-reminder","revision":1,"status":"inProgress","taskId":"01a0eed3-9d0a-7591-bea5-563258797d0b","turnId":"01a0eed3-9490-7d1f-9d49-37a21c167827"},"sessionId":"01a0eed3-932b-73e3-af13-d270ac13c13e","sourceRange":{"first":{"id":"74a8a721-4c79-4f69-8ff6-45ed99f86cad","sequence":70},"last":{"id":"74a8a721-4c79-4f69-8ff6-45ed99f86cad","sequence":70},"stream":{"id":"01a0eed3-932b-73e3-af13-d270ac13c13e","kind":"session"}},"viewCursor":"v:01a0eed3-932b-73e3-af13-d270ac13c13e:8"}},{"method":"item/completed","params":{"item":{"childSessionId":"3d09af71-0bcc-4b94-ba8f-5acf92f259fe","fallbackText":"Reminder child session","generationId":1,"itemId":"74a8a721-4c79-4f69-8ff6-45ed99f86cad","kind":"reminderChild","recordedAt":"2026-09-29T20:21:07.122102Z","reminderAgentId":"verify-reminder","revision":2,"status":"completed","taskId":"01a0eed3-9d0a-7591-bea5-563258797d0b","turnId":"01a0eed3-9490-7d1f-9d49-37a21c167827"},"sessionId":"01a0eed3-932b-73e3-af13-d270ac13c13e","sourceRange":{"first":{"id":"74a8a721-4c79-4f69-8ff6-45ed99f86cad","sequence":70},"last":{"id":"4516461e-5fa2-439d-9b4e-8ad009827cef","sequence":72},"stream":{"id":"01a0eed3-932b-73e3-af13-d270ac13c13e","kind":"session"}},"viewCursor":"v:01a0eed3-932b-73e3-af13-d270ac13c13e:9"}},{"method":"item/completed","params":{"item":{"childSessionId":"cec38684-5da4-4884-a207-ef7834c0c389","failureReason":"main run completed","fallbackText":"Reminder child session","generationId":1,"itemId":"5e203ac7-881e-4731-b6dc-a887c5763cce","kind":"reminderChild","recordedAt":"2026-09-29T20:21:07.124683Z","reminderAgentId":"skill-reminder","revision":2,"status":"cancelled","taskId":"01a0eed3-94a8-7051-a7b4-675143c10378","turnId":"01a0eed3-9490-7d1f-9d49-37a21c167827"},"sessionId":"01a0eed3-932b-73e3-af13-d270ac13c13e","sourceRange":{"first":{"id":"5e203ac7-881e-4731-b6dc-a887c5763cce","sequence":47},"last":{"id":"127a52d4-8345-4b21-8b2b-2de5acd43aa9","sequence":76},"stream":{"id":"01a0eed3-932b-73e3-af13-d270ac13c13e","kind":"session"}},"viewCursor":"v:01a0eed3-932b-73e3-af13-d270ac13c13e:10"}},{"method":"turn/completed","params":{"durationMs":28445,"sessionId":"01a0eed3-932b-73e3-af13-d270ac13c13e","sourceRange":{"first":{"id":"24aabfdb-72c9-4bf7-9bb6-0ea2d464e1b3","sequence":78},"last":{"id":"24aabfdb-72c9-4bf7-9bb6-0ea2d464e1b3","sequence":78},"stream":{"id":"01a0eed3-932b-73e3-af13-d270ac13c13e","kind":"session"}},"terminal":"completed","timeToFirstTokenMs":2101,"turnId":"01a0eed3-9490-7d1f-9d49-37a21c167827","viewCursor":"v:01a0eed3-932b-73e3-af13-d270ac13c13e:11"}},{"method":"session/branchChanged","params":{"branch":"main","sessionId":"01a0eed3-932b-73e3-af13-d270ac13c13e","sourceRange":{"first":{"id":"ce7e1c0f-ddda-4972-b534-bc92924685f8","sequence":82},"last":{"id":"ce7e1c0f-ddda-4972-b534-bc92924685f8","sequence":82},"stream":{"id":"01a0eed3-932b-73e3-af13-d270ac13c13e","kind":"session"}},"vcs":"git","viewCursor":"v:01a0eed3-932b-73e3-af13-d270ac13c13e:12","workspaceRoot":"/tmp/muse-parity/repo"}},{"method":"turn/started","params":{"commandId":"01a0eed5-7b87-763f-98b5-aa3bcf41b646","sessionId":"01a0eed3-932b-73e3-af13-d270ac13c13e","sourceRange":{"first":{"id":"4e92c557-fea3-4adb-b9f1-a219e2766de2","sequence":83},"last":{"id":"4e92c557-fea3-4adb-b9f1-a219e2766de2","sequence":83},"stream":{"id":"01a0eed3-932b-73e3-af13-d270ac13c13e","kind":"session"}},"turnId":"01a0eed5-7b87-763f-98b5-aa3bcf41b646","viewCursor":"v:01a0eed3-932b-73e3-af13-d270ac13c13e:13"}},{"method":"item/completed","params":{"item":{"commandId":"01a0eed5-7b87-763f-98b5-aa3bcf41b646","itemId":"fc181aa2-5c85-4575-9c3a-a651ab9478c5","kind":"userMessage","recordedAt":"2026-09-29T20:22:43.338938Z","revision":1,"status":"completed","text":"/muse-parity-user","turnId":"01a0eed5-7b87-763f-98b5-aa3bcf41b646"},"sessionId":"01a0eed3-932b-73e3-af13-d270ac13c13e","sourceRange":{"first":{"id":"4e92c557-fea3-4adb-b9f1-a219e2766de2","sequence":83},"last":{"id":"4e92c557-fea3-4adb-b9f1-a219e2766de2","sequence":83},"stream":{"id":"01a0eed3-932b-73e3-af13-d270ac13c13e","kind":"session"}},"viewCursor":"v:01a0eed3-932b-73e3-af13-d270ac13c13e:14"}},{"method":"item/started","params":{"item":{"childSessionId":"fad33bda-d96d-4fa3-88ae-f81530005c1c","fallbackText":"Reminder child session","generationId":1,"itemId":"0ee04e13-90e5-4cb2-af03-b6652be140bb","kind":"reminderChild","recordedAt":"2026-09-29T20:22:43.34489Z","reminderAgentId":"skill-reminder","revision":1,"status":"inProgress","taskId":"01a0eed5-7b90-78c1-9bb3-3e0964c21058","turnId":"01a0eed5-7b87-763f-98b5-aa3bcf41b646"},"sessionId":"01a0eed3-932b-73e3-af13-d270ac13c13e","sourceRange":{"first":{"id":"0ee04e13-90e5-4cb2-af03-b6652be140bb","sequence":101},"last":{"id":"0ee04e13-90e5-4cb2-af03-b6652be140bb","sequence":101},"stream":{"id":"01a0eed3-932b-73e3-af13-d270ac13c13e","kind":"session"}},"viewCursor":"v:01a0eed3-932b-73e3-af13-d270ac13c13e:15"}},{"method":"item/completed","params":{"item":{"itemId":"1fb15644-4718-4e8f-9de1-07c7a2aed493","kind":"agentMessage","recordedAt":"2026-09-29T20:22:46.666881Z","revision":2,"status":"completed","text":"MUSE_PARITY_USER_OK","turnId":"01a0eed5-7b87-763f-98b5-aa3bcf41b646"},"sessionId":"01a0eed3-932b-73e3-af13-d270ac13c13e","sourceRange":{"first":{"id":"285cf5ec-9f46-4432-8783-3a26bf8f227d","sequence":114},"last":{"id":"285cf5ec-9f46-4432-8783-3a26bf8f227d","sequence":114},"stream":{"id":"01a0eed3-932b-73e3-af13-d270ac13c13e","kind":"session"}},"viewCursor":"v:01a0eed3-932b-73e3-af13-d270ac13c13e:16"}},{"method":"session/tokenUsage","params":{"cumulative":{"outputTokens":69,"promptTokens":84939,"totalTokens":85008},"durationMs":3323,"modelId":"meta/muse-spark-1.3","promptTokens":42563,"sessionId":"01a0eed3-932b-73e3-af13-d270ac13c13e","sourceRange":{"first":{"id":"04716cb9-ff28-4cdb-92ed-673336fe8e6c","sequence":117},"last":{"id":"04716cb9-ff28-4cdb-92ed-673336fe8e6c","sequence":117},"stream":{"id":"01a0eed3-932b-73e3-af13-d270ac13c13e","kind":"session"}},"totalTokens":42604,"turnId":"01a0eed5-7b87-763f-98b5-aa3bcf41b646","usage":{"cacheReadTokens":42353,"cacheWriteTokens":0,"cachedTokens":42353,"inputTokens":42563,"outputTokens":41,"reasoningTokens":25},"viewCursor":"v:01a0eed3-932b-73e3-af13-d270ac13c13e:17"}},{"method":"item/started","params":{"item":{"childSessionId":"a1cb2525-f5b0-47b2-a1ff-78e559d92aaf","fallbackText":"Reminder child session","generationId":1,"itemId":"5edfc05e-e503-416c-8b57-fdae2bb628be","kind":"reminderChild","recordedAt":"2026-09-29T20:22:46.672229Z","reminderAgentId":"verify-reminder","revision":1,"status":"inProgress","taskId":"01a0eed5-888f-7e03-88be-47ead1dc0403","turnId":"01a0eed5-7b87-763f-98b5-aa3bcf41b646"},"sessionId":"01a0eed3-932b-73e3-af13-d270ac13c13e","sourceRange":{"first":{"id":"5edfc05e-e503-416c-8b57-fdae2bb628be","sequence":124},"last":{"id":"5edfc05e-e503-416c-8b57-fdae2bb628be","sequence":124},"stream":{"id":"01a0eed3-932b-73e3-af13-d270ac13c13e","kind":"session"}},"viewCursor":"v:01a0eed3-932b-73e3-af13-d270ac13c13e:18"}},{"method":"item/completed","params":{"item":{"childSessionId":"fad33bda-d96d-4fa3-88ae-f81530005c1c","failureReason":"main run completed","fallbackText":"Reminder child session","generationId":1,"itemId":"0ee04e13-90e5-4cb2-af03-b6652be140bb","kind":"reminderChild","recordedAt":"2026-09-29T20:23:49.319829Z","reminderAgentId":"skill-reminder","revision":2,"status":"cancelled","taskId":"01a0eed5-7b90-78c1-9bb3-3e0964c21058","turnId":"01a0eed5-7b87-763f-98b5-aa3bcf41b646"},"sessionId":"01a0eed3-932b-73e3-af13-d270ac13c13e","sourceRange":{"first":{"id":"0ee04e13-90e5-4cb2-af03-b6652be140bb","sequence":101},"last":{"id":"6a3021bc-7c2f-4578-8f7c-ca8fbc48d075","sequence":128},"stream":{"id":"01a0eed3-932b-73e3-af13-d270ac13c13e","kind":"session"}},"viewCursor":"v:01a0eed3-932b-73e3-af13-d270ac13c13e:19"}},{"method":"item/completed","params":{"item":{"childSessionId":"a1cb2525-f5b0-47b2-a1ff-78e559d92aaf","failureReason":"main run completed","fallbackText":"Reminder child session","generationId":1,"itemId":"5edfc05e-e503-416c-8b57-fdae2bb628be","kind":"reminderChild","recordedAt":"2026-09-29T20:23:49.319989Z","reminderAgentId":"verify-reminder","revision":2,"status":"cancelled","taskId":"01a0eed5-888f-7e03-88be-47ead1dc0403","turnId":"01a0eed5-7b87-763f-98b5-aa3bcf41b646"},"sessionId":"01a0eed3-932b-73e3-af13-d270ac13c13e","sourceRange":{"first":{"id":"5edfc05e-e503-416c-8b57-fdae2bb628be","sequence":124},"last":{"id":"a0001d86-c30d-4f13-899c-0c5754bc371e","sequence":129},"stream":{"id":"01a0eed3-932b-73e3-af13-d270ac13c13e","kind":"session"}},"viewCursor":"v:01a0eed3-932b-73e3-af13-d270ac13c13e:20"}},{"method":"turn/completed","params":{"durationMs":65981,"sessionId":"01a0eed3-932b-73e3-af13-d270ac13c13e","sourceRange":{"first":{"id":"281374ab-9801-41bf-a86e-48f4ae38a114","sequence":132},"last":{"id":"281374ab-9801-41bf-a86e-48f4ae38a114","sequence":132},"stream":{"id":"01a0eed3-932b-73e3-af13-d270ac13c13e","kind":"session"}},"terminal":"completed","timeToFirstTokenMs":3284,"turnId":"01a0eed5-7b87-763f-98b5-aa3bcf41b646","viewCursor":"v:01a0eed3-932b-73e3-af13-d270ac13c13e:21"}},{"method":"turn/started","params":{"commandId":"01a0eed8-16b2-77b8-ae22-9455e0b6804a","sessionId":"01a0eed3-932b-73e3-af13-d270ac13c13e","sourceRange":{"first":{"id":"020df195-c790-42c0-9620-334ad004ceb2","sequence":138},"last":{"id":"020df195-c790-42c0-9620-334ad004ceb2","sequence":138},"stream":{"id":"01a0eed3-932b-73e3-af13-d270ac13c13e","kind":"session"}},"turnId":"01a0eed8-16b2-77b8-ae22-9455e0b6804a","viewCursor":"v:01a0eed3-932b-73e3-af13-d270ac13c13e:22"}},{"method":"item/completed","params":{"item":{"commandId":"01a0eed8-16b2-77b8-ae22-9455e0b6804a","itemId":"94a3b885-3b56-4ccb-b78d-36a0871fb9ad","kind":"userMessage","recordedAt":"2026-09-29T20:25:34.132571Z","revision":1,"status":"completed","text":"Spawn one native subagent in this same workspace, without a worktree. Ask it to reply MUSE_PARITY_CHILD_OK without tools. Wait for it and read its result. Then report the exact result.","turnId":"01a0eed8-16b2-77b8-ae22-9455e0b6804a"},"sessionId":"01a0eed3-932b-73e3-af13-d270ac13c13e","sourceRange":{"first":{"id":"020df195-c790-42c0-9620-334ad004ceb2","sequence":138},"last":{"id":"020df195-c790-42c0-9620-334ad004ceb2","sequence":138},"stream":{"id":"01a0eed3-932b-73e3-af13-d270ac13c13e","kind":"session"}},"viewCursor":"v:01a0eed3-932b-73e3-af13-d270ac13c13e:23"}},{"method":"item/started","params":{"item":{"childSessionId":"bc91fcc4-0b2f-431b-a95d-3bc02197624e","fallbackText":"Reminder child session","generationId":1,"itemId":"e79c416d-2da6-4ebb-8f48-bfb821a04033","kind":"reminderChild","recordedAt":"2026-09-29T20:25:34.141088Z","reminderAgentId":"skill-reminder","revision":1,"status":"inProgress","taskId":"01a0eed8-16bb-7bd3-9e27-ffdecd0dd526","turnId":"01a0eed8-16b2-77b8-ae22-9455e0b6804a"},"sessionId":"01a0eed3-932b-73e3-af13-d270ac13c13e","sourceRange":{"first":{"id":"e79c416d-2da6-4ebb-8f48-bfb821a04033","sequence":155},"last":{"id":"e79c416d-2da6-4ebb-8f48-bfb821a04033","sequence":155},"stream":{"id":"01a0eed3-932b-73e3-af13-d270ac13c13e","kind":"session"}},"viewCursor":"v:01a0eed3-932b-73e3-af13-d270ac13c13e:24"}},{"method":"item/completed","params":{"item":{"itemId":"6c408c57-e8c2-4361-a23f-96492e13ec5f","kind":"reasoning","providerItemId":"rs_6abc1ec2d96d7e128bf2443f:rs_01a0eed827c4737aa8620c2f582a17a2","recordedAt":"2026-09-29T20:25:45.516664Z","revision":2,"status":"completed","summary":["Evaluating the native subagent spawn via workflow tool and output constraint.Selecting the workflow tool as the first step to spawn the native subagent."],"turnId":"01a0eed8-16b2-77b8-ae22-9455e0b6804a"},"sessionId":"01a0eed3-932b-73e3-af13-d270ac13c13e","sourceRange":{"first":{"id":"24e4ccb9-18f2-40d1-b630-2ed9a5ba91ad","sequence":169},"last":{"id":"24e4ccb9-18f2-40d1-b630-2ed9a5ba91ad","sequence":169},"stream":{"id":"01a0eed3-932b-73e3-af13-d270ac13c13e","kind":"session"}},"viewCursor":"v:01a0eed3-932b-73e3-af13-d270ac13c13e:25"}},{"method":"session/tokenUsage","params":{"cumulative":{"outputTokens":817,"promptTokens":127591,"totalTokens":128408},"durationMs":12002,"finishReason":"tool_calls","modelId":"meta/muse-spark-1.3","promptTokens":42652,"sessionId":"01a0eed3-932b-73e3-af13-d270ac13c13e","sourceRange":{"first":{"id":"ef2bd965-531c-4f07-9957-c0c481c7627c","sequence":174},"last":{"id":"ef2bd965-531c-4f07-9957-c0c481c7627c","sequence":174},"stream":{"id":"01a0eed3-932b-73e3-af13-d270ac13c13e","kind":"session"}},"totalTokens":43400,"turnId":"01a0eed8-16b2-77b8-ae22-9455e0b6804a","usage":{"cacheReadTokens":42481,"cacheWriteTokens":0,"cachedTokens":42481,"inputTokens":42652,"outputTokens":748,"reasoningTokens":558},"viewCursor":"v:01a0eed3-932b-73e3-af13-d270ac13c13e:26"}},{"method":"item/started","params":{"item":{"args":"{\"args\":null,\"name\":\"spawn parity child\",\"script\":\"export default async function workflow(host) { const result = await host.agent({ input: \\\"Reply exactly MUSE_PARITY_CHILD_OK. Do not use tools.\\\" }); return { status: \\\"ok\\\", ref: result.ref, text: result.text }; }\"}","callId":"call_01a0eed8426e710da463d6b58a683556","itemId":"01a0eed8-45a3-7a90-bd9f-dd7825ca7d73","kind":"toolCall","recordedAt":"2026-09-29T20:25:46.147985Z","revision":1,"status":"inProgress","tool":"workflow","turnId":"01a0eed8-16b2-77b8-ae22-9455e0b6804a"},"sessionId":"01a0eed3-932b-73e3-af13-d270ac13c13e","sourceRange":{"first":{"id":"a918854d-ea32-4bc2-8d4a-335312d095a8","sequence":176},"last":{"id":"b516abf5-88df-4487-9bee-5f90f988e3a8","sequence":182},"stream":{"id":"01a0eed3-932b-73e3-af13-d270ac13c13e","kind":"session"}},"viewCursor":"v:01a0eed3-932b-73e3-af13-d270ac13c13e:27"}},{"method":"item/completed","params":{"item":{"args":"{\"args\":null,\"name\":\"spawn parity child\",\"script\":\"export default async function workflow(host) { const result = await host.agent({ input: \\\"Reply exactly MUSE_PARITY_CHILD_OK. Do not use tools.\\\" }); return { status: \\\"ok\\\", ref: result.ref, text: result.text }; }\"}","callId":"call_01a0eed8426e710da463d6b58a683556","itemId":"01a0eed8-45a3-7a90-bd9f-dd7825ca7d73","kind":"toolCall","recordedAt":"2026-09-29T20:25:46.154672Z","revision":2,"status":"completed","tool":"workflow","turnId":"01a0eed8-16b2-77b8-ae22-9455e0b6804a","visibleOutput":"{\"args\":null,\"entryId\":\"spawn parity child\",\"hostApiVersion\":\"v1\",\"maxParallelAgents\":14,\"message\":\"Workflow launched: the workflow runs in the background and its completion is delivered to this conversation. Do not call workflow again while this owner is live. The persisted script lives at /tmp/muse-workflow-session-QQ6yi5/workflows/scripts/01a0eed8-45a4-7553-95ac-4b2d977782c7.js. To iterate after this owner ends (completion, failure, or reported recovery), edit that file and call workflow again with {\\\"name\\\": \\\"spawn parity child\\\", \\\"scriptPath\\\": \\\"/tmp/muse-workflow-session-QQ6yi5/workflows/scripts/01a0eed8-45a4-7553-95ac-4b2d977782c7.js\\\", \\\"resumeFromRunId\\\": \\\"workflow-run-model-tool-call_01a0eed8426e710da463d6b58a683556\\\"} instead of resending inline script; a resume re-executes the script from the top and reuses only the longest unchanged completed call prefix, so completed agent() calls before your first change replay from the journal instead of re-running. Runtime admission (policy, caps, duplicates) may still reject a resume. To stop this run before it completes, call work_stop with work_id = work.v1.workflow.sha256.7ef8923b4c9481e5fd18eda03337c24e338bc27c81453b74d45b34372050dec6.\",\"policy\":{\"caps\":\"caps:generated-js-workflow\",\"childLimit\":14,\"id\":\"policy:standard\"},\"scriptBytes\":210,\"scriptHash\":\"sha256:8a896fd97908248633764a409ec0ba5a17a0df3cf01d0fff31ecd6023ad9e8a1\",\"scriptPath\":\"/tmp/muse-workflow-session-QQ6yi5/workflows/scripts/01a0eed8-45a4-7553-95ac-4b2d977782c7.js\",\"status\":\"launched\",\"taskId\":\"01a0eed8-45a4-7553-95ac-4b2d977782c7\",\"tokenBudget\":null,\"workId\":\"work.v1.workflow.sha256.7ef8923b4c9481e5fd18eda03337c24e338bc27c81453b74d45b34372050dec6\",\"workflowRunId\":\"workflow-run-model-tool-call_01a0eed8426e710da463d6b58a683556\",\"workflowRunIdDescription\":\"Internal opaque control handle for Workflow calls only. Pass this exact value only as resumeFromRunId; never repeat it in user-facing prose.\"}"},"sessionId":"01a0eed3-932b-73e3-af13-d270ac13c13e","sourceRange":{"first":{"id":"a918854d-ea32-4bc2-8d4a-335312d095a8","sequence":176},"last":{"id":"447a11b5-a5aa-474e-bcda-3e6f013de031","sequence":186},"stream":{"id":"01a0eed3-932b-73e3-af13-d270ac13c13e","kind":"session"}},"viewCursor":"v:01a0eed3-932b-73e3-af13-d270ac13c13e:28"}},{"method":"item/started","params":{"item":{"children":[],"entryId":"spawn parity child","fallbackText":"Workflow: model-chosen generated workflow","itemId":"31dba18d-857b-4162-afc0-7883f5aebc0e","kind":"workflow","recordedAt":"2026-09-29T20:25:46.154908Z","revision":1,"scriptId":"generated.workflow.spawn_parity_child","status":"inProgress","triggerSource":"guidanceAuto","turnId":"01a0eed8-16b2-77b8-ae22-9455e0b6804a","workflowRunId":"workflow-run-model-tool-call_01a0eed8426e710da463d6b58a683556"},"sessionId":"01a0eed3-932b-73e3-af13-d270ac13c13e","sourceRange":{"first":{"id":"31dba18d-857b-4162-afc0-7883f5aebc0e","sequence":187},"last":{"id":"31dba18d-857b-4162-afc0-7883f5aebc0e","sequence":187},"stream":{"id":"01a0eed3-932b-73e3-af13-d270ac13c13e","kind":"session"}},"viewCursor":"v:01a0eed3-932b-73e3-af13-d270ac13c13e:29"}},{"method":"item/completed","params":{"item":{"childSessionId":"bc91fcc4-0b2f-431b-a95d-3bc02197624e","failureReason":"main run completed","fallbackText":"Reminder child session","generationId":1,"itemId":"e79c416d-2da6-4ebb-8f48-bfb821a04033","kind":"reminderChild","recordedAt":"2026-09-29T20:25:46.164296Z","reminderAgentId":"skill-reminder","revision":2,"status":"cancelled","taskId":"01a0eed8-16bb-7bd3-9e27-ffdecd0dd526","turnId":"01a0eed8-16b2-77b8-ae22-9455e0b6804a"},"sessionId":"01a0eed3-932b-73e3-af13-d270ac13c13e","sourceRange":{"first":{"id":"e79c416d-2da6-4ebb-8f48-bfb821a04033","sequence":155},"last":{"id":"d30ba6fe-0058-4c76-bdcd-db8442f4f8a3","sequence":199},"stream":{"id":"01a0eed3-932b-73e3-af13-d270ac13c13e","kind":"session"}},"viewCursor":"v:01a0eed3-932b-73e3-af13-d270ac13c13e:30"}},{"method":"turn/completed","params":{"durationMs":12032,"sessionId":"01a0eed3-932b-73e3-af13-d270ac13c13e","sourceRange":{"first":{"id":"0959cbda-3c7e-4d82-a400-7f344204e6e4","sequence":201},"last":{"id":"0959cbda-3c7e-4d82-a400-7f344204e6e4","sequence":201},"stream":{"id":"01a0eed3-932b-73e3-af13-d270ac13c13e","kind":"session"}},"terminal":"completed","timeToFirstTokenMs":6515,"turnId":"01a0eed8-16b2-77b8-ae22-9455e0b6804a","viewCursor":"v:01a0eed3-932b-73e3-af13-d270ac13c13e:31"}},{"method":"item/updated","params":{"item":{"children":[{"attempt":1,"childId":"01a0eed8-45b6-7810-a015-af925aa86416","status":"scheduled"}],"entryId":"spawn parity child","fallbackText":"Workflow: model-chosen generated workflow","itemId":"31dba18d-857b-4162-afc0-7883f5aebc0e","kind":"workflow","recordedAt":"2026-09-29T20:25:46.168395Z","revision":2,"scriptId":"generated.workflow.spawn_parity_child","status":"inProgress","triggerSource":"guidanceAuto","turnId":"01a0eed8-16b2-77b8-ae22-9455e0b6804a","workflowRunId":"workflow-run-model-tool-call_01a0eed8426e710da463d6b58a683556"},"sessionId":"01a0eed3-932b-73e3-af13-d270ac13c13e","sourceRange":{"first":{"id":"31dba18d-857b-4162-afc0-7883f5aebc0e","sequence":187},"last":{"id":"8d852df5-bab8-467e-a534-00466a62eb73","sequence":203},"stream":{"id":"01a0eed3-932b-73e3-af13-d270ac13c13e","kind":"session"}},"viewCursor":"v:01a0eed3-932b-73e3-af13-d270ac13c13e:32"}},{"method":"item/updated","params":{"item":{"children":[{"attempt":1,"childId":"01a0eed8-45b6-7810-a015-af925aa86416","status":"started"}],"entryId":"spawn parity child","fallbackText":"Workflow: model-chosen generated workflow","itemId":"31dba18d-857b-4162-afc0-7883f5aebc0e","kind":"workflow","recordedAt":"2026-09-29T20:25:46.176812Z","revision":3,"scriptId":"generated.workflow.spawn_parity_child","status":"inProgress","triggerSource":"guidanceAuto","turnId":"01a0eed8-16b2-77b8-ae22-9455e0b6804a","workflowRunId":"workflow-run-model-tool-call_01a0eed8426e710da463d6b58a683556"},"sessionId":"01a0eed3-932b-73e3-af13-d270ac13c13e","sourceRange":{"first":{"id":"31dba18d-857b-4162-afc0-7883f5aebc0e","sequence":187},"last":{"id":"4feba775-86a0-4dcb-be62-e922a6f2cb1b","sequence":215},"stream":{"id":"01a0eed3-932b-73e3-af13-d270ac13c13e","kind":"session"}},"viewCursor":"v:01a0eed3-932b-73e3-af13-d270ac13c13e:33"}},{"method":"item/updated","params":{"item":{"children":[{"attempt":1,"childId":"01a0eed8-45b6-7810-a015-af925aa86416","status":"usage","usage":{"cacheReadTokens":0,"cacheWriteTokens":0,"cachedTokens":0,"inputTokens":36383,"outputTokens":246,"reasoningTokens":184}}],"entryId":"spawn parity child","fallbackText":"Workflow: model-chosen generated workflow","itemId":"31dba18d-857b-4162-afc0-7883f5aebc0e","kind":"workflow","recordedAt":"2026-09-29T20:26:03.529253Z","revision":4,"scriptId":"generated.workflow.spawn_parity_child","status":"inProgress","triggerSource":"guidanceAuto","turnId":"01a0eed8-16b2-77b8-ae22-9455e0b6804a","workflowRunId":"workflow-run-model-tool-call_01a0eed8426e710da463d6b58a683556"},"sessionId":"01a0eed3-932b-73e3-af13-d270ac13c13e","sourceRange":{"first":{"id":"31dba18d-857b-4162-afc0-7883f5aebc0e","sequence":187},"last":{"id":"6a319567-897f-4423-8808-54683c29d18b","sequence":217},"stream":{"id":"01a0eed3-932b-73e3-af13-d270ac13c13e","kind":"session"}},"viewCursor":"v:01a0eed3-932b-73e3-af13-d270ac13c13e:34"}},{"method":"item/updated","params":{"item":{"children":[{"attempt":1,"childId":"01a0eed8-45b6-7810-a015-af925aa86416","durationMs":17340,"resultRef":"subagent-result://01a0eed8-45b6-7810-a015-af925aa86416/task/4d18b0eb-a557-5a5f-b101-1fde09ebd4e2#5","status":"completed"}],"entryId":"spawn parity child","fallbackText":"Workflow: model-chosen generated workflow","itemId":"31dba18d-857b-4162-afc0-7883f5aebc0e","kind":"workflow","recordedAt":"2026-09-29T20:26:03.529999Z","revision":5,"scriptId":"generated.workflow.spawn_parity_child","status":"inProgress","triggerSource":"guidanceAuto","turnId":"01a0eed8-16b2-77b8-ae22-9455e0b6804a","workflowRunId":"workflow-run-model-tool-call_01a0eed8426e710da463d6b58a683556"},"sessionId":"01a0eed3-932b-73e3-af13-d270ac13c13e","sourceRange":{"first":{"id":"31dba18d-857b-4162-afc0-7883f5aebc0e","sequence":187},"last":{"id":"437d1f27-fb59-4905-aad5-d90c2e45d0af","sequence":220},"stream":{"id":"01a0eed3-932b-73e3-af13-d270ac13c13e","kind":"session"}},"viewCursor":"v:01a0eed3-932b-73e3-af13-d270ac13c13e:35"}},{"method":"item/updated","params":{"item":{"children":[{"attempt":1,"childId":"01a0eed8-45b6-7810-a015-af925aa86416","durationMs":17340,"resultRef":"subagent-result://01a0eed8-45b6-7810-a015-af925aa86416/task/4d18b0eb-a557-5a5f-b101-1fde09ebd4e2#5","status":"terminal","terminal":"completed"}],"entryId":"spawn parity child","fallbackText":"Workflow: model-chosen generated workflow","itemId":"31dba18d-857b-4162-afc0-7883f5aebc0e","kind":"workflow","recordedAt":"2026-09-29T20:26:03.530175Z","revision":6,"scriptId":"generated.workflow.spawn_parity_child","status":"inProgress","triggerSource":"guidanceAuto","turnId":"01a0eed8-16b2-77b8-ae22-9455e0b6804a","workflowRunId":"workflow-run-model-tool-call_01a0eed8426e710da463d6b58a683556"},"sessionId":"01a0eed3-932b-73e3-af13-d270ac13c13e","sourceRange":{"first":{"id":"31dba18d-857b-4162-afc0-7883f5aebc0e","sequence":187},"last":{"id":"feef6547-fed1-467b-aa1d-50714586bebd","sequence":221},"stream":{"id":"01a0eed3-932b-73e3-af13-d270ac13c13e","kind":"session"}},"viewCursor":"v:01a0eed3-932b-73e3-af13-d270ac13c13e:36"}},{"method":"item/completed","params":{"item":{"children":[{"attempt":1,"childId":"01a0eed8-45b6-7810-a015-af925aa86416","durationMs":17340,"resultRef":"subagent-result://01a0eed8-45b6-7810-a015-af925aa86416/task/4d18b0eb-a557-5a5f-b101-1fde09ebd4e2#5","status":"terminal","terminal":"completed"}],"entryId":"spawn parity child","fallbackText":"Workflow: model-chosen generated workflow","itemId":"31dba18d-857b-4162-afc0-7883f5aebc0e","kind":"workflow","message":"{\"agents_activity\":[{\"agent\":\"01a0eed8-45b6-7810-a015-af925aa86416\",\"duration_ms\":17340,\"tool_calls\":0}],\"call_id\":\"call_01a0eed8426e710da463d6b58a683556\",\"child_work_started\":true,\"deferred_to_background\":false,\"final_summary\":{\"status\":\"completed\",\"summary\":\"MUSE_PARITY_CHILD_OK\"},\"final_summary_instruction\":\"Use final_summary.summary as the completed workflow output.\",\"latest_failure\":null,\"launch_admitted\":true,\"launch_command_id\":\"2fdf4f42-54b3-4f4e-8b7c-2e5b88c0df4d\",\"production_owner_loop_started\":true,\"provider_or_tool_io_started\":true,\"route\":\"runtime command admission -> production owner loop\",\"type\":\"workflow_launch_reconciled\",\"workspace_handoffs\":[{\"agent\":\"01a0eed8-45b6-7810-a015-af925aa86416\",\"description\":\"Subagent workspace: shared\"}]}","recordedAt":"2026-09-29T20:26:03.535398Z","revision":7,"scriptId":"generated.workflow.spawn_parity_child","status":"completed","triggerSource":"guidanceAuto","turnId":"01a0eed8-16b2-77b8-ae22-9455e0b6804a","workflowRunId":"workflow-run-model-tool-call_01a0eed8426e710da463d6b58a683556"},"sessionId":"01a0eed3-932b-73e3-af13-d270ac13c13e","sourceRange":{"first":{"id":"31dba18d-857b-4162-afc0-7883f5aebc0e","sequence":187},"last":{"id":"a3221d26-4614-4c00-b599-f7a0a33e21f6","sequence":223},"stream":{"id":"01a0eed3-932b-73e3-af13-d270ac13c13e","kind":"session"}},"viewCursor":"v:01a0eed3-932b-73e3-af13-d270ac13c13e:37"}},{"method":"turn/started","params":{"commandId":"01a0eed8-8990-7271-9e30-32ac5633d609","sessionId":"01a0eed3-932b-73e3-af13-d270ac13c13e","sourceRange":{"first":{"id":"b0f895bf-adcd-4057-a01e-b5c9bdeff3bf","sequence":229},"last":{"id":"b0f895bf-adcd-4057-a01e-b5c9bdeff3bf","sequence":229},"stream":{"id":"01a0eed3-932b-73e3-af13-d270ac13c13e","kind":"session"}},"turnId":"01a0eed8-8990-7271-9e30-32ac5633d609","viewCursor":"v:01a0eed3-932b-73e3-af13-d270ac13c13e:38"}},{"method":"item/started","params":{"item":{"childSessionId":"4ed57256-2cd8-4626-9920-4717fc722540","fallbackText":"Reminder child session","generationId":1,"itemId":"39e56ac4-800d-42fc-9c7d-3930ba386e6a","kind":"reminderChild","recordedAt":"2026-09-29T20:26:03.549717Z","reminderAgentId":"skill-reminder","revision":1,"status":"inProgress","taskId":"01a0eed8-899c-7df1-b4cd-51819d9a5ef7","turnId":"01a0eed8-8990-7271-9e30-32ac5633d609"},"sessionId":"01a0eed3-932b-73e3-af13-d270ac13c13e","sourceRange":{"first":{"id":"39e56ac4-800d-42fc-9c7d-3930ba386e6a","sequence":246},"last":{"id":"39e56ac4-800d-42fc-9c7d-3930ba386e6a","sequence":246},"stream":{"id":"01a0eed3-932b-73e3-af13-d270ac13c13e","kind":"session"}},"viewCursor":"v:01a0eed3-932b-73e3-af13-d270ac13c13e:39"}},{"method":"item/completed","params":{"item":{"itemId":"fd03f853-b1b6-4e17-9027-93b822ab461f","kind":"agentMessage","recordedAt":"2026-09-29T20:26:06.821505Z","revision":2,"status":"completed","text":"MUSE_PARITY_CHILD_OK","turnId":"01a0eed8-8990-7271-9e30-32ac5633d609"},"sessionId":"01a0eed3-932b-73e3-af13-d270ac13c13e","sourceRange":{"first":{"id":"002a4c90-a9d5-4af5-9964-9471e2ae2c93","sequence":259},"last":{"id":"002a4c90-a9d5-4af5-9964-9471e2ae2c93","sequence":259},"stream":{"id":"01a0eed3-932b-73e3-af13-d270ac13c13e","kind":"session"}},"viewCursor":"v:01a0eed3-932b-73e3-af13-d270ac13c13e:40"}},{"method":"session/tokenUsage","params":{"cumulative":{"outputTokens":925,"promptTokens":172385,"totalTokens":173310},"durationMs":3274,"modelId":"meta/muse-spark-1.3","promptTokens":44794,"sessionId":"01a0eed3-932b-73e3-af13-d270ac13c13e","sourceRange":{"first":{"id":"4803220f-dc93-4ef2-8997-681916e54764","sequence":262},"last":{"id":"4803220f-dc93-4ef2-8997-681916e54764","sequence":262},"stream":{"id":"01a0eed3-932b-73e3-af13-d270ac13c13e","kind":"session"}},"totalTokens":44902,"turnId":"01a0eed8-8990-7271-9e30-32ac5633d609","usage":{"cacheReadTokens":0,"cacheWriteTokens":0,"cachedTokens":0,"inputTokens":44794,"outputTokens":108,"reasoningTokens":92},"viewCursor":"v:01a0eed3-932b-73e3-af13-d270ac13c13e:41"}},{"method":"item/started","params":{"item":{"childSessionId":"12ef4c41-c0c1-43fd-8488-7a8518a5a72b","fallbackText":"Reminder child session","generationId":1,"itemId":"d9fb16ad-4169-49f7-b13f-01a9190f23f1","kind":"reminderChild","recordedAt":"2026-09-29T20:26:06.828633Z","reminderAgentId":"verify-reminder","revision":1,"status":"inProgress","taskId":"01a0eed8-966c-73e0-af5c-2ddb56432d01","turnId":"01a0eed8-8990-7271-9e30-32ac5633d609"},"sessionId":"01a0eed3-932b-73e3-af13-d270ac13c13e","sourceRange":{"first":{"id":"d9fb16ad-4169-49f7-b13f-01a9190f23f1","sequence":269},"last":{"id":"d9fb16ad-4169-49f7-b13f-01a9190f23f1","sequence":269},"stream":{"id":"01a0eed3-932b-73e3-af13-d270ac13c13e","kind":"session"}},"viewCursor":"v:01a0eed3-932b-73e3-af13-d270ac13c13e:42"}},{"method":"item/completed","params":{"item":{"childSessionId":"12ef4c41-c0c1-43fd-8488-7a8518a5a72b","fallbackText":"Reminder child session","generationId":1,"itemId":"d9fb16ad-4169-49f7-b13f-01a9190f23f1","kind":"reminderChild","recordedAt":"2026-09-29T20:26:12.073884Z","reminderAgentId":"verify-reminder","revision":2,"status":"completed","taskId":"01a0eed8-966c-73e0-af5c-2ddb56432d01","turnId":"01a0eed8-8990-7271-9e30-32ac5633d609"},"sessionId":"01a0eed3-932b-73e3-af13-d270ac13c13e","sourceRange":{"first":{"id":"d9fb16ad-4169-49f7-b13f-01a9190f23f1","sequence":269},"last":{"id":"83355680-dff1-4c14-84e7-b1e2fbabfdd7","sequence":271},"stream":{"id":"01a0eed3-932b-73e3-af13-d270ac13c13e","kind":"session"}},"viewCursor":"v:01a0eed3-932b-73e3-af13-d270ac13c13e:43"}},{"method":"item/completed","params":{"item":{"childSessionId":"4ed57256-2cd8-4626-9920-4717fc722540","failureReason":"main run completed","fallbackText":"Reminder child session","generationId":1,"itemId":"39e56ac4-800d-42fc-9c7d-3930ba386e6a","kind":"reminderChild","recordedAt":"2026-09-29T20:26:12.075841Z","reminderAgentId":"skill-reminder","revision":2,"status":"cancelled","taskId":"01a0eed8-899c-7df1-b4cd-51819d9a5ef7","turnId":"01a0eed8-8990-7271-9e30-32ac5633d609"},"sessionId":"01a0eed3-932b-73e3-af13-d270ac13c13e","sourceRange":{"first":{"id":"39e56ac4-800d-42fc-9c7d-3930ba386e6a","sequence":246},"last":{"id":"1395704d-186b-42cb-81c9-1b196b799db1","sequence":275},"stream":{"id":"01a0eed3-932b-73e3-af13-d270ac13c13e","kind":"session"}},"viewCursor":"v:01a0eed3-932b-73e3-af13-d270ac13c13e:44"}},{"method":"turn/completed","params":{"durationMs":8538,"sessionId":"01a0eed3-932b-73e3-af13-d270ac13c13e","sourceRange":{"first":{"id":"10ca1fba-6e0b-4878-bf32-dffbd98da848","sequence":277},"last":{"id":"10ca1fba-6e0b-4878-bf32-dffbd98da848","sequence":277},"stream":{"id":"01a0eed3-932b-73e3-af13-d270ac13c13e","kind":"session"}},"terminal":"completed","timeToFirstTokenMs":3005,"turnId":"01a0eed8-8990-7271-9e30-32ac5633d609","viewCursor":"v:01a0eed3-932b-73e3-af13-d270ac13c13e:45"}},{"method":"turn/started","params":{"commandId":"01a0eedb-985c-7976-95be-bd698ad7c26f","sessionId":"01a0eed3-932b-73e3-af13-d270ac13c13e","sourceRange":{"first":{"id":"c396b9cf-5aaa-4575-ad13-3ea92f45c7d9","sequence":283},"last":{"id":"c396b9cf-5aaa-4575-ad13-3ea92f45c7d9","sequence":283},"stream":{"id":"01a0eed3-932b-73e3-af13-d270ac13c13e","kind":"session"}},"turnId":"01a0eedb-985c-7976-95be-bd698ad7c26f","viewCursor":"v:01a0eed3-932b-73e3-af13-d270ac13c13e:46"}},{"method":"item/completed","params":{"item":{"commandId":"01a0eedb-985c-7976-95be-bd698ad7c26f","itemId":"35ac342e-8c07-49b5-b49b-cbc94b77550a","kind":"userMessage","recordedAt":"2026-09-29T20:29:23.935493Z","revision":1,"status":"completed","text":"Use the native subagent_spawn tool (not workflow) to spawn one child in shared placement, with the objective to reply MUSE_NATIVE_CHILD_OK without tools. Then use subagent_wait and subagent_read_result. Do not fabricate a child response. If those three tools are unavailable, report that fact and stop.","turnId":"01a0eedb-985c-7976-95be-bd698ad7c26f"},"sessionId":"01a0eed3-932b-73e3-af13-d270ac13c13e","sourceRange":{"first":{"id":"c396b9cf-5aaa-4575-ad13-3ea92f45c7d9","sequence":283},"last":{"id":"c396b9cf-5aaa-4575-ad13-3ea92f45c7d9","sequence":283},"stream":{"id":"01a0eed3-932b-73e3-af13-d270ac13c13e","kind":"session"}},"viewCursor":"v:01a0eed3-932b-73e3-af13-d270ac13c13e:47"}},{"method":"item/started","params":{"item":{"childSessionId":"40841c92-f7f0-49d0-be79-e9f8b49840ae","fallbackText":"Reminder child session","generationId":1,"itemId":"88f27e2d-73f0-4cef-8df9-136407dfccbc","kind":"reminderChild","recordedAt":"2026-09-29T20:29:23.949687Z","reminderAgentId":"skill-reminder","revision":1,"status":"inProgress","taskId":"01a0eedb-986c-7af3-85f1-1969b4bf35e8","turnId":"01a0eedb-985c-7976-95be-bd698ad7c26f"},"sessionId":"01a0eed3-932b-73e3-af13-d270ac13c13e","sourceRange":{"first":{"id":"88f27e2d-73f0-4cef-8df9-136407dfccbc","sequence":300},"last":{"id":"88f27e2d-73f0-4cef-8df9-136407dfccbc","sequence":300},"stream":{"id":"01a0eed3-932b-73e3-af13-d270ac13c13e","kind":"session"}},"viewCursor":"v:01a0eed3-932b-73e3-af13-d270ac13c13e:48"}},{"method":"item/completed","params":{"item":{"itemId":"4a5cdd4a-85eb-4cc7-8caa-0e1e074765b6","kind":"reasoning","providerItemId":"rs_6abc1fa9318445219611407e:rs_01a0eedbadf67749aa0fe24c380cefed","recordedAt":"2026-09-29T20:29:32.463434Z","revision":2,"status":"completed","summary":["Checking that requested subagent tools are unavailable and ruling out alternatives."],"turnId":"01a0eedb-985c-7976-95be-bd698ad7c26f"},"sessionId":"01a0eed3-932b-73e3-af13-d270ac13c13e","sourceRange":{"first":{"id":"c2e789ef-79d2-4315-ad7a-69eb1647db73","sequence":313},"last":{"id":"c2e789ef-79d2-4315-ad7a-69eb1647db73","sequence":313},"stream":{"id":"01a0eed3-932b-73e3-af13-d270ac13c13e","kind":"session"}},"viewCursor":"v:01a0eed3-932b-73e3-af13-d270ac13c13e:49"}},{"method":"item/completed","params":{"item":{"itemId":"48382df7-a7f2-4905-b7a5-1e1f38e72e39","kind":"agentMessage","recordedAt":"2026-09-29T20:29:32.781524Z","revision":2,"status":"completed","text":"Those three tools are unavailable: no `subagent_spawn`, `subagent_wait`, or `subagent_read_result` in this session. Stopping without spawning.","turnId":"01a0eedb-985c-7976-95be-bd698ad7c26f"},"sessionId":"01a0eed3-932b-73e3-af13-d270ac13c13e","sourceRange":{"first":{"id":"2ea66af3-381a-4c98-877b-b42ca42b7fcc","sequence":315},"last":{"id":"2ea66af3-381a-4c98-877b-b42ca42b7fcc","sequence":315},"stream":{"id":"01a0eed3-932b-73e3-af13-d270ac13c13e","kind":"session"}},"viewCursor":"v:01a0eed3-932b-73e3-af13-d270ac13c13e:50"}},{"method":"session/tokenUsage","params":{"cumulative":{"outputTokens":1228,"promptTokens":217357,"totalTokens":218585},"durationMs":8856,"modelId":"meta/muse-spark-1.3","promptTokens":44972,"sessionId":"01a0eed3-932b-73e3-af13-d270ac13c13e","sourceRange":{"first":{"id":"3ef736cb-c187-4840-9626-b02a570d41a7","sequence":318},"last":{"id":"3ef736cb-c187-4840-9626-b02a570d41a7","sequence":318},"stream":{"id":"01a0eed3-932b-73e3-af13-d270ac13c13e","kind":"session"}},"totalTokens":45275,"turnId":"01a0eedb-985c-7976-95be-bd698ad7c26f","usage":{"cacheReadTokens":42609,"cacheWriteTokens":0,"cachedTokens":42609,"inputTokens":44972,"outputTokens":303,"reasoningTokens":243},"viewCursor":"v:01a0eed3-932b-73e3-af13-d270ac13c13e:51"}},{"method":"item/started","params":{"item":{"childSessionId":"24e649bd-9271-4204-ad6d-1ef9afae2148","fallbackText":"Reminder child session","generationId":1,"itemId":"df7811bb-bb7c-4a3d-9014-917e213aaac0","kind":"reminderChild","recordedAt":"2026-09-29T20:29:32.809722Z","reminderAgentId":"verify-reminder","revision":1,"status":"inProgress","taskId":"01a0eedb-bb09-7771-a0a4-706b31da0407","turnId":"01a0eedb-985c-7976-95be-bd698ad7c26f"},"sessionId":"01a0eed3-932b-73e3-af13-d270ac13c13e","sourceRange":{"first":{"id":"df7811bb-bb7c-4a3d-9014-917e213aaac0","sequence":325},"last":{"id":"df7811bb-bb7c-4a3d-9014-917e213aaac0","sequence":325},"stream":{"id":"01a0eed3-932b-73e3-af13-d270ac13c13e","kind":"session"}},"viewCursor":"v:01a0eed3-932b-73e3-af13-d270ac13c13e:52"}},{"method":"item/completed","params":{"item":{"childSessionId":"24e649bd-9271-4204-ad6d-1ef9afae2148","fallbackText":"Reminder child session","generationId":1,"itemId":"df7811bb-bb7c-4a3d-9014-917e213aaac0","kind":"reminderChild","recordedAt":"2026-09-29T20:29:35.684855Z","reminderAgentId":"verify-reminder","revision":2,"status":"completed","taskId":"01a0eedb-bb09-7771-a0a4-706b31da0407","turnId":"01a0eedb-985c-7976-95be-bd698ad7c26f"},"sessionId":"01a0eed3-932b-73e3-af13-d270ac13c13e","sourceRange":{"first":{"id":"df7811bb-bb7c-4a3d-9014-917e213aaac0","sequence":325},"last":{"id":"d9bf6a05-53e3-46f4-ba20-16d911fe33f2","sequence":327},"stream":{"id":"01a0eed3-932b-73e3-af13-d270ac13c13e","kind":"session"}},"viewCursor":"v:01a0eed3-932b-73e3-af13-d270ac13c13e:53"}},{"method":"item/completed","params":{"item":{"childSessionId":"40841c92-f7f0-49d0-be79-e9f8b49840ae","failureReason":"main run completed","fallbackText":"Reminder child session","generationId":1,"itemId":"88f27e2d-73f0-4cef-8df9-136407dfccbc","kind":"reminderChild","recordedAt":"2026-09-29T20:29:35.687891Z","reminderAgentId":"skill-reminder","revision":2,"status":"cancelled","taskId":"01a0eedb-986c-7af3-85f1-1969b4bf35e8","turnId":"01a0eedb-985c-7976-95be-bd698ad7c26f"},"sessionId":"01a0eed3-932b-73e3-af13-d270ac13c13e","sourceRange":{"first":{"id":"88f27e2d-73f0-4cef-8df9-136407dfccbc","sequence":300},"last":{"id":"aec8adeb-28ee-4d80-a48a-63d15b1bb509","sequence":331},"stream":{"id":"01a0eed3-932b-73e3-af13-d270ac13c13e","kind":"session"}},"viewCursor":"v:01a0eed3-932b-73e3-af13-d270ac13c13e:54"}},{"method":"turn/completed","params":{"durationMs":11752,"sessionId":"01a0eed3-932b-73e3-af13-d270ac13c13e","sourceRange":{"first":{"id":"0a61a65a-910b-4a3e-8f89-fa7b709a1fef","sequence":333},"last":{"id":"0a61a65a-910b-4a3e-8f89-fa7b709a1fef","sequence":333},"stream":{"id":"01a0eed3-932b-73e3-af13-d270ac13c13e","kind":"session"}},"terminal":"completed","timeToFirstTokenMs":8175,"turnId":"01a0eedb-985c-7976-95be-bd698ad7c26f","viewCursor":"v:01a0eed3-932b-73e3-af13-d270ac13c13e:55"}}],"nextCursor":null}}} diff --git a/plugins/muse-provider/test/fixtures/phase3-project-skill.ndjson b/plugins/muse-provider/test/fixtures/phase3-project-skill.ndjson new file mode 100644 index 0000000000..5030cf7712 --- /dev/null +++ b/plugins/muse-provider/test/fixtures/phase3-project-skill.ndjson @@ -0,0 +1,34 @@ +{"dir":"out","msg":{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"clientInfo":{"name":"paseo_phase0","version":"0.0.0"},"capabilities":{"experimentalApi":true,"requestedCapabilities":["sessionMcp","sessionListStream"]}}}} +{"dir":"in","msg":{"id":1,"jsonrpc":"2.0","result":{"experimentalApi":true,"grantedCapabilities":["sessionMcp","sessionListStream"],"museHome":"/tmp/muse-phase3/data/muse","platformFamily":"unix","platformOs":"linux","schema":{"fingerprint":"sha256:e0e163db6ccf00dbe68402ce55d6319b3edc33c421f31e9583b587b2de8a118f","version":1},"serverInfo":{"name":"muse","version":"1.4.1"},"sessionDurability":"durable","userAgent":"muse-build/1.4.1 (non-interactive; linux-x86_64; build 35815c253477406321e3f5595195becc25e3907a)"}}} +{"dir":"out","msg":{"jsonrpc":"2.0","method":"initialized","params":{}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":2,"method":"session/start","params":{"commandId":"01a0eed7-ce1c-7a41-a57e-8b7af8ece3f2","workspaceRoot":"/tmp/muse-phase3/repo","modelId":"meta/muse-spark-1.3","providerId":"meta","approvalMode":"onRequest"}}} +{"dir":"in","msg":{"emittedAtMs":1790713515636,"jsonrpc":"2.0","method":"session/started","params":{"session":{"activeTurnId":null,"approvalMode":{"lastCommandId":null,"mode":"onRequest","source":"startup"},"createdAt":"2026-09-29T20:25:15.558112Z","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase3/data/muse/sessions/2026/09/29/01a0eed7-ce23-75e2-b14d-7e7261cde078/session.jsonl","providerId":"meta","sessionId":"01a0eed7-ce23-75e2-b14d-7e7261cde078","status":"idle","turnCount":0,"updatedAt":"2026-09-29T20:25:15.558116Z","workspaceRoot":"/tmp/muse-phase3/repo"}}}} +{"dir":"in","msg":{"id":2,"jsonrpc":"2.0","result":{"session":{"activeTurnId":null,"approvalMode":{"lastCommandId":null,"mode":"onRequest","source":"startup"},"createdAt":"2026-09-29T20:25:15.558112Z","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase3/data/muse/sessions/2026/09/29/01a0eed7-ce23-75e2-b14d-7e7261cde078/session.jsonl","providerId":"meta","sessionId":"01a0eed7-ce23-75e2-b14d-7e7261cde078","status":"idle","turnCount":0,"updatedAt":"2026-09-29T20:25:15.558116Z","workspaceRoot":"/tmp/muse-phase3/repo"},"viewCursor":"v:01a0eed7-ce23-75e2-b14d-7e7261cde078:1"}}} +{"dir":"in","msg":{"emittedAtMs":1790713515643,"jsonrpc":"2.0","method":"session/approvalModeChanged","params":{"clientName":"paseo_phase0","commandId":"01a0eed7-ce1c-7a41-a57e-8b7af8ece3f2","mode":"onRequest","sessionId":"01a0eed7-ce23-75e2-b14d-7e7261cde078","source":"approvalReconfigure","sourceRange":{"first":{"id":"c5190deb-8c15-4324-8cab-bcc21767d0f2","sequence":5},"last":{"id":"c5190deb-8c15-4324-8cab-bcc21767d0f2","sequence":5},"stream":{"id":"01a0eed7-ce23-75e2-b14d-7e7261cde078","kind":"session"}},"viewCursor":"v:01a0eed7-ce23-75e2-b14d-7e7261cde078:1"}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":3,"method":"skill/list","params":{"sessionId":"01a0eed7-ce23-75e2-b14d-7e7261cde078"}}} +{"dir":"in","msg":{"id":3,"jsonrpc":"2.0","result":{"skills":[{"description":"Muse parity project skill","displayName":"muse-parity-project","selector":"muse-parity-project","source":"project"},{"description":"Muse parity user skill","displayName":"muse-parity-user","selector":"muse-parity-user","source":"user"}]}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":4,"method":"turn/start","params":{"commandId":"01a0eed7-ce81-7503-ab0a-30ce06f2ea83","sessionId":"01a0eed7-ce23-75e2-b14d-7e7261cde078","input":[{"type":"skill","selector":"muse-parity-project","arguments":"QA"}],"reasoningEffort":"low"}}} +{"dir":"in","msg":{"emittedAtMs":1790713515652,"jsonrpc":"2.0","method":"session/branchChanged","params":{"branch":"main","sessionId":"01a0eed7-ce23-75e2-b14d-7e7261cde078","sourceRange":{"first":{"id":"8e8dbc61-209b-4238-a059-0e84b658d2ce","sequence":14},"last":{"id":"8e8dbc61-209b-4238-a059-0e84b658d2ce","sequence":14},"stream":{"id":"01a0eed7-ce23-75e2-b14d-7e7261cde078","kind":"session"}},"vcs":"git","viewCursor":"v:01a0eed7-ce23-75e2-b14d-7e7261cde078:2","workspaceRoot":"/tmp/muse-phase3/repo"}}} +{"dir":"in","msg":{"id":4,"jsonrpc":"2.0","result":{"commandId":"01a0eed7-ce81-7503-ab0a-30ce06f2ea83","disposition":"started","startedNewTurn":true,"status":"accepted","turnId":"01a0eed7-ce81-7503-ab0a-30ce06f2ea83"}}} +{"dir":"in","msg":{"emittedAtMs":1790713515660,"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a0eed7-ce23-75e2-b14d-7e7261cde078","status":"running","viewCursor":"v:01a0eed7-ce23-75e2-b14d-7e7261cde078:3"}}} +{"dir":"in","msg":{"emittedAtMs":1790713515661,"jsonrpc":"2.0","method":"turn/started","params":{"commandId":"01a0eed7-ce81-7503-ab0a-30ce06f2ea83","sessionId":"01a0eed7-ce23-75e2-b14d-7e7261cde078","sourceRange":{"first":{"id":"15fad29e-01e3-44eb-8c62-d1c6f42a15fd","sequence":15},"last":{"id":"15fad29e-01e3-44eb-8c62-d1c6f42a15fd","sequence":15},"stream":{"id":"01a0eed7-ce23-75e2-b14d-7e7261cde078","kind":"session"}},"turnId":"01a0eed7-ce81-7503-ab0a-30ce06f2ea83","viewCursor":"v:01a0eed7-ce23-75e2-b14d-7e7261cde078:3"}}} +{"dir":"in","msg":{"emittedAtMs":1790713515661,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"commandId":"01a0eed7-ce81-7503-ab0a-30ce06f2ea83","itemId":"e682051f-db33-497f-b64a-984279221acc","kind":"userMessage","recordedAt":"2026-09-29T20:25:15.653086Z","revision":1,"status":"completed","text":"/muse-parity-project QA","turnId":"01a0eed7-ce81-7503-ab0a-30ce06f2ea83"},"sessionId":"01a0eed7-ce23-75e2-b14d-7e7261cde078","sourceRange":{"first":{"id":"15fad29e-01e3-44eb-8c62-d1c6f42a15fd","sequence":15},"last":{"id":"15fad29e-01e3-44eb-8c62-d1c6f42a15fd","sequence":15},"stream":{"id":"01a0eed7-ce23-75e2-b14d-7e7261cde078","kind":"session"}},"viewCursor":"v:01a0eed7-ce23-75e2-b14d-7e7261cde078:4"}}} +{"dir":"in","msg":{"emittedAtMs":1790713515661,"jsonrpc":"2.0","method":"item/started","params":{"item":{"childSessionId":"048a0237-11cc-4e2e-910e-a15d8a4c0b8a","fallbackText":"Reminder child session","generationId":1,"itemId":"a05c1a91-8de9-4107-af6a-11b60b9e51d5","kind":"reminderChild","recordedAt":"2026-09-29T20:25:15.661569Z","reminderAgentId":"skill-reminder","revision":1,"status":"inProgress","taskId":"01a0eed7-ce8c-7d13-a2d7-bdaeba350f66","turnId":"01a0eed7-ce81-7503-ab0a-30ce06f2ea83"},"sessionId":"01a0eed7-ce23-75e2-b14d-7e7261cde078","sourceRange":{"first":{"id":"a05c1a91-8de9-4107-af6a-11b60b9e51d5","sequence":34},"last":{"id":"a05c1a91-8de9-4107-af6a-11b60b9e51d5","sequence":34},"stream":{"id":"01a0eed7-ce23-75e2-b14d-7e7261cde078","kind":"session"}},"viewCursor":"v:01a0eed7-ce23-75e2-b14d-7e7261cde078:5"}}} +{"dir":"in","msg":{"emittedAtMs":1790713515666,"jsonrpc":"2.0","method":"session/listChanged","params":{"session":{"activeTurnId":"01a0eed7-ce81-7503-ab0a-30ce06f2ea83","createdAt":"2026-09-29T20:25:15.558112Z","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase3/data/muse/sessions/2026/09/29/01a0eed7-ce23-75e2-b14d-7e7261cde078/session.jsonl","providerId":"meta","sessionId":"01a0eed7-ce23-75e2-b14d-7e7261cde078","status":"running","turnCount":0,"updatedAt":"2026-09-29T20:25:15.66081Z","workspaceRoot":"/tmp/muse-phase3/repo"}}}} +{"dir":"in","msg":{"emittedAtMs":1790713517353,"jsonrpc":"2.0","method":"item/started","params":{"item":{"itemId":"6e5ecd1e-53fe-4a9b-be7b-ceeaa7aca348","kind":"agentMessage","revision":1,"status":"inProgress","text":"","turnId":"01a0eed7-ce81-7503-ab0a-30ce06f2ea83"},"sessionId":"01a0eed7-ce23-75e2-b14d-7e7261cde078","viewCursor":"v:01a0eed7-ce23-75e2-b14d-7e7261cde078:5.1"}}} +{"dir":"in","msg":{"emittedAtMs":1790713517353,"jsonrpc":"2.0","method":"item/delta","params":{"delta":"MUSE_PARITY","field":"text","itemId":"6e5ecd1e-53fe-4a9b-be7b-ceeaa7aca348","sessionId":"01a0eed7-ce23-75e2-b14d-7e7261cde078","viewCursor":"v:01a0eed7-ce23-75e2-b14d-7e7261cde078:5.2"}}} +{"dir":"in","msg":{"emittedAtMs":1790713517597,"jsonrpc":"2.0","method":"item/delta","params":{"delta":"_PROJECT_OK","field":"text","itemId":"6e5ecd1e-53fe-4a9b-be7b-ceeaa7aca348","sessionId":"01a0eed7-ce23-75e2-b14d-7e7261cde078","viewCursor":"v:01a0eed7-ce23-75e2-b14d-7e7261cde078:5.3"}}} +{"dir":"in","msg":{"emittedAtMs":1790713517642,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"itemId":"6e5ecd1e-53fe-4a9b-be7b-ceeaa7aca348","kind":"agentMessage","recordedAt":"2026-09-29T20:25:17.642147Z","revision":2,"status":"completed","text":"MUSE_PARITY_PROJECT_OK","turnId":"01a0eed7-ce81-7503-ab0a-30ce06f2ea83"},"sessionId":"01a0eed7-ce23-75e2-b14d-7e7261cde078","sourceRange":{"first":{"id":"da4ee9f0-e30b-4312-97e1-7ad4bf3e0ba6","sequence":47},"last":{"id":"da4ee9f0-e30b-4312-97e1-7ad4bf3e0ba6","sequence":47},"stream":{"id":"01a0eed7-ce23-75e2-b14d-7e7261cde078","kind":"session"}},"viewCursor":"v:01a0eed7-ce23-75e2-b14d-7e7261cde078:6"}}} +{"dir":"in","msg":{"emittedAtMs":1790713517646,"jsonrpc":"2.0","method":"session/tokenUsage","params":{"cumulative":{"outputTokens":30,"promptTokens":38319,"totalTokens":38349},"durationMs":1980,"modelId":"meta/muse-spark-1.3","promptTokens":38319,"sessionId":"01a0eed7-ce23-75e2-b14d-7e7261cde078","sourceRange":{"first":{"id":"65b08952-8f7e-4f00-b9dd-7e549d0a711e","sequence":50},"last":{"id":"65b08952-8f7e-4f00-b9dd-7e549d0a711e","sequence":50},"stream":{"id":"01a0eed7-ce23-75e2-b14d-7e7261cde078","kind":"session"}},"totalTokens":38349,"turnId":"01a0eed7-ce81-7503-ab0a-30ce06f2ea83","usage":{"cacheReadTokens":0,"cacheWriteTokens":0,"cachedTokens":0,"inputTokens":38319,"outputTokens":30,"reasoningTokens":14},"viewCursor":"v:01a0eed7-ce23-75e2-b14d-7e7261cde078:7"}}} +{"dir":"in","msg":{"emittedAtMs":1790713517648,"jsonrpc":"2.0","method":"item/started","params":{"item":{"childSessionId":"6d2f855f-101e-4114-adf5-3672b6d749b9","childSessionLogPath":"subagent/6d2f855f-101e-4114-adf5-3672b6d749b9/session.jsonl","fallbackText":"Reminder child session","generationId":1,"itemId":"c05b349f-2b67-4550-a142-0d408f3cf37f","kind":"reminderChild","recordedAt":"2026-09-29T20:25:17.648165Z","reminderAgentId":"verify-reminder","revision":1,"status":"inProgress","taskId":"01a0eed7-d64e-7e33-8f45-142f02afde1d","turnId":"01a0eed7-ce81-7503-ab0a-30ce06f2ea83"},"sessionId":"01a0eed7-ce23-75e2-b14d-7e7261cde078","sourceRange":{"first":{"id":"c05b349f-2b67-4550-a142-0d408f3cf37f","sequence":57},"last":{"id":"c05b349f-2b67-4550-a142-0d408f3cf37f","sequence":57},"stream":{"id":"01a0eed7-ce23-75e2-b14d-7e7261cde078","kind":"session"}},"viewCursor":"v:01a0eed7-ce23-75e2-b14d-7e7261cde078:8"}}} +{"dir":"in","msg":{"emittedAtMs":1790713529181,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"childSessionId":"048a0237-11cc-4e2e-910e-a15d8a4c0b8a","fallbackText":"Reminder child session","generationId":1,"itemId":"a05c1a91-8de9-4107-af6a-11b60b9e51d5","kind":"reminderChild","recordedAt":"2026-09-29T20:25:29.181248Z","reminderAgentId":"skill-reminder","revision":2,"status":"completed","taskId":"01a0eed7-ce8c-7d13-a2d7-bdaeba350f66","turnId":"01a0eed7-ce81-7503-ab0a-30ce06f2ea83"},"sessionId":"01a0eed7-ce23-75e2-b14d-7e7261cde078","sourceRange":{"first":{"id":"a05c1a91-8de9-4107-af6a-11b60b9e51d5","sequence":34},"last":{"id":"f1d3ebd8-0f87-43b9-aefa-c8cd4f39faa6","sequence":59},"stream":{"id":"01a0eed7-ce23-75e2-b14d-7e7261cde078","kind":"session"}},"viewCursor":"v:01a0eed7-ce23-75e2-b14d-7e7261cde078:9"}}} +{"dir":"in","msg":{"emittedAtMs":1790713529181,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"childSessionId":"6d2f855f-101e-4114-adf5-3672b6d749b9","childSessionLogPath":"subagent/6d2f855f-101e-4114-adf5-3672b6d749b9/session.jsonl","fallbackText":"Reminder child session","generationId":1,"itemId":"c05b349f-2b67-4550-a142-0d408f3cf37f","kind":"reminderChild","recordedAt":"2026-09-29T20:25:29.181659Z","reminderAgentId":"verify-reminder","revision":2,"status":"completed","taskId":"01a0eed7-d64e-7e33-8f45-142f02afde1d","turnId":"01a0eed7-ce81-7503-ab0a-30ce06f2ea83"},"sessionId":"01a0eed7-ce23-75e2-b14d-7e7261cde078","sourceRange":{"first":{"id":"c05b349f-2b67-4550-a142-0d408f3cf37f","sequence":57},"last":{"id":"d879311c-d5a9-43a3-94a6-dff55ae38a7b","sequence":60},"stream":{"id":"01a0eed7-ce23-75e2-b14d-7e7261cde078","kind":"session"}},"viewCursor":"v:01a0eed7-ce23-75e2-b14d-7e7261cde078:10"}}} +{"dir":"in","msg":{"emittedAtMs":1790713529183,"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a0eed7-ce23-75e2-b14d-7e7261cde078","status":"idle","viewCursor":"v:01a0eed7-ce23-75e2-b14d-7e7261cde078:11"}}} +{"dir":"in","msg":{"emittedAtMs":1790713529184,"jsonrpc":"2.0","method":"turn/completed","params":{"durationMs":13530,"sessionId":"01a0eed7-ce23-75e2-b14d-7e7261cde078","sourceRange":{"first":{"id":"4a0e2be3-61af-4a7e-a49a-3da3bd98791e","sequence":66},"last":{"id":"4a0e2be3-61af-4a7e-a49a-3da3bd98791e","sequence":66},"stream":{"id":"01a0eed7-ce23-75e2-b14d-7e7261cde078","kind":"session"}},"terminal":"completed","timeToFirstTokenMs":1700,"turnId":"01a0eed7-ce81-7503-ab0a-30ce06f2ea83","viewCursor":"v:01a0eed7-ce23-75e2-b14d-7e7261cde078:11"}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":5,"method":"usage/read","params":{}}} +{"dir":"in","msg":{"id":5,"jsonrpc":"2.0","result":{}}} +{"dir":"in","msg":{"emittedAtMs":1790713529184,"jsonrpc":"2.0","method":"session/branchChanged","params":{"branch":"main","sessionId":"01a0eed7-ce23-75e2-b14d-7e7261cde078","sourceRange":{"first":{"id":"08e62d29-771c-4cf4-b186-34fb91ec6810","sequence":67},"last":{"id":"08e62d29-771c-4cf4-b186-34fb91ec6810","sequence":67},"stream":{"id":"01a0eed7-ce23-75e2-b14d-7e7261cde078","kind":"session"}},"vcs":"git","viewCursor":"v:01a0eed7-ce23-75e2-b14d-7e7261cde078:12","workspaceRoot":"/tmp/muse-phase3/repo"}}} +{"dir":"in","msg":{"emittedAtMs":1790713529184,"jsonrpc":"2.0","method":"session/listChanged","params":{"session":{"activeTurnId":null,"branch":"main","createdAt":"2026-09-29T20:25:15.555669Z","firstUserPrompt":"/muse-parity-project QA","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase3/data/muse/sessions/2026/09/29/01a0eed7-ce23-75e2-b14d-7e7261cde078/session.jsonl","providerId":"meta","sessionId":"01a0eed7-ce23-75e2-b14d-7e7261cde078","status":"idle","title":"/muse-parity-project QA","turnCount":1,"updatedAt":"2026-09-29T20:25:29.183321Z","workspaceRoot":"/tmp/muse-phase3/repo"}}}} +{"dir":"in","msg":{"emittedAtMs":1790713529442,"jsonrpc":"2.0","method":"session/closed","params":{"reason":"hostShutdown","sessionId":"01a0eed7-ce23-75e2-b14d-7e7261cde078","viewCursor":"v:01a0eed7-ce23-75e2-b14d-7e7261cde078:12"}}} +{"dir":"in","msg":{"emittedAtMs":1790713529442,"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a0eed7-ce23-75e2-b14d-7e7261cde078","status":"notLoaded","viewCursor":"v:01a0eed7-ce23-75e2-b14d-7e7261cde078:12"}}} +{"dir":"in","msg":{"emittedAtMs":1790713529442,"jsonrpc":"2.0","method":"session/listChanged","params":{"session":{"activeTurnId":null,"branch":"main","createdAt":"2026-09-29T20:25:15.555669Z","firstUserPrompt":"/muse-parity-project QA","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase3/data/muse/sessions/2026/09/29/01a0eed7-ce23-75e2-b14d-7e7261cde078/session.jsonl","providerId":"meta","sessionId":"01a0eed7-ce23-75e2-b14d-7e7261cde078","status":"notLoaded","title":"/muse-parity-project QA","turnCount":1,"updatedAt":"2026-09-29T20:25:29.184532Z","workspaceRoot":"/tmp/muse-phase3/repo"}}}} diff --git a/plugins/muse-provider/test/fixtures/phase3-user-skill.ndjson b/plugins/muse-provider/test/fixtures/phase3-user-skill.ndjson new file mode 100644 index 0000000000..3deee1b6a4 --- /dev/null +++ b/plugins/muse-provider/test/fixtures/phase3-user-skill.ndjson @@ -0,0 +1,32 @@ +{"dir":"out","msg":{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"clientInfo":{"name":"paseo_phase0","version":"0.0.0"},"capabilities":{"experimentalApi":true,"requestedCapabilities":["sessionMcp","sessionListStream"]}}}} +{"dir":"in","msg":{"id":1,"jsonrpc":"2.0","result":{"experimentalApi":true,"grantedCapabilities":["sessionMcp","sessionListStream"],"museHome":"/tmp/muse-phase3/data/muse","platformFamily":"unix","platformOs":"linux","schema":{"fingerprint":"sha256:e0e163db6ccf00dbe68402ce55d6319b3edc33c421f31e9583b587b2de8a118f","version":1},"serverInfo":{"name":"muse","version":"1.4.1"},"sessionDurability":"durable","userAgent":"muse-build/1.4.1 (non-interactive; linux-x86_64; build 35815c253477406321e3f5595195becc25e3907a)"}}} +{"dir":"out","msg":{"jsonrpc":"2.0","method":"initialized","params":{}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":2,"method":"session/start","params":{"commandId":"01a0eed7-6eae-71dd-aa98-bd57366a4d2a","workspaceRoot":"/tmp/muse-phase3/repo","modelId":"meta/muse-spark-1.3","providerId":"meta","approvalMode":"onRequest"}}} +{"dir":"in","msg":{"emittedAtMs":1790713491207,"jsonrpc":"2.0","method":"session/started","params":{"session":{"activeTurnId":null,"approvalMode":{"lastCommandId":null,"mode":"onRequest","source":"startup"},"createdAt":"2026-09-29T20:24:51.128331Z","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase3/data/muse/sessions/2026/09/29/01a0eed7-6eb5-7490-b19e-7053a0a27c2d/session.jsonl","providerId":"meta","sessionId":"01a0eed7-6eb5-7490-b19e-7053a0a27c2d","status":"idle","turnCount":0,"updatedAt":"2026-09-29T20:24:51.128335Z","workspaceRoot":"/tmp/muse-phase3/repo"}}}} +{"dir":"in","msg":{"id":2,"jsonrpc":"2.0","result":{"session":{"activeTurnId":null,"approvalMode":{"lastCommandId":null,"mode":"onRequest","source":"startup"},"createdAt":"2026-09-29T20:24:51.128331Z","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase3/data/muse/sessions/2026/09/29/01a0eed7-6eb5-7490-b19e-7053a0a27c2d/session.jsonl","providerId":"meta","sessionId":"01a0eed7-6eb5-7490-b19e-7053a0a27c2d","status":"idle","turnCount":0,"updatedAt":"2026-09-29T20:24:51.128335Z","workspaceRoot":"/tmp/muse-phase3/repo"},"viewCursor":"v:01a0eed7-6eb5-7490-b19e-7053a0a27c2d:1"}}} +{"dir":"in","msg":{"emittedAtMs":1790713491215,"jsonrpc":"2.0","method":"session/approvalModeChanged","params":{"clientName":"paseo_phase0","commandId":"01a0eed7-6eae-71dd-aa98-bd57366a4d2a","mode":"onRequest","sessionId":"01a0eed7-6eb5-7490-b19e-7053a0a27c2d","source":"approvalReconfigure","sourceRange":{"first":{"id":"903ccb69-7421-43e4-bb85-b661fc9ef2a5","sequence":5},"last":{"id":"903ccb69-7421-43e4-bb85-b661fc9ef2a5","sequence":5},"stream":{"id":"01a0eed7-6eb5-7490-b19e-7053a0a27c2d","kind":"session"}},"viewCursor":"v:01a0eed7-6eb5-7490-b19e-7053a0a27c2d:1"}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":3,"method":"skill/list","params":{"sessionId":"01a0eed7-6eb5-7490-b19e-7053a0a27c2d"}}} +{"dir":"in","msg":{"id":3,"jsonrpc":"2.0","result":{"skills":[{"description":"Muse parity user skill","displayName":"muse-parity-user","selector":"muse-parity-user","source":"user"}]}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":4,"method":"turn/start","params":{"commandId":"01a0eed7-6f16-7497-ab8e-8bd0733e2f6d","sessionId":"01a0eed7-6eb5-7490-b19e-7053a0a27c2d","input":[{"type":"skill","selector":"muse-parity-user","arguments":"QA"}],"reasoningEffort":"low"}}} +{"dir":"in","msg":{"emittedAtMs":1790713491224,"jsonrpc":"2.0","method":"session/branchChanged","params":{"branch":"main","sessionId":"01a0eed7-6eb5-7490-b19e-7053a0a27c2d","sourceRange":{"first":{"id":"54e74beb-b4e8-40d0-bb11-52abd1345cf1","sequence":13},"last":{"id":"54e74beb-b4e8-40d0-bb11-52abd1345cf1","sequence":13},"stream":{"id":"01a0eed7-6eb5-7490-b19e-7053a0a27c2d","kind":"session"}},"vcs":"git","viewCursor":"v:01a0eed7-6eb5-7490-b19e-7053a0a27c2d:2","workspaceRoot":"/tmp/muse-phase3/repo"}}} +{"dir":"in","msg":{"id":4,"jsonrpc":"2.0","result":{"commandId":"01a0eed7-6f16-7497-ab8e-8bd0733e2f6d","disposition":"started","startedNewTurn":true,"status":"accepted","turnId":"01a0eed7-6f16-7497-ab8e-8bd0733e2f6d"}}} +{"dir":"in","msg":{"emittedAtMs":1790713491232,"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a0eed7-6eb5-7490-b19e-7053a0a27c2d","status":"running","viewCursor":"v:01a0eed7-6eb5-7490-b19e-7053a0a27c2d:3"}}} +{"dir":"in","msg":{"emittedAtMs":1790713491233,"jsonrpc":"2.0","method":"turn/started","params":{"commandId":"01a0eed7-6f16-7497-ab8e-8bd0733e2f6d","sessionId":"01a0eed7-6eb5-7490-b19e-7053a0a27c2d","sourceRange":{"first":{"id":"ab071d7e-7b44-401a-b8ec-32f213c94289","sequence":14},"last":{"id":"ab071d7e-7b44-401a-b8ec-32f213c94289","sequence":14},"stream":{"id":"01a0eed7-6eb5-7490-b19e-7053a0a27c2d","kind":"session"}},"turnId":"01a0eed7-6f16-7497-ab8e-8bd0733e2f6d","viewCursor":"v:01a0eed7-6eb5-7490-b19e-7053a0a27c2d:3"}}} +{"dir":"in","msg":{"emittedAtMs":1790713491233,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"commandId":"01a0eed7-6f16-7497-ab8e-8bd0733e2f6d","itemId":"2efada29-ee84-4446-b6af-2bbbb76c6424","kind":"userMessage","recordedAt":"2026-09-29T20:24:51.22541Z","revision":1,"status":"completed","text":"/muse-parity-user QA","turnId":"01a0eed7-6f16-7497-ab8e-8bd0733e2f6d"},"sessionId":"01a0eed7-6eb5-7490-b19e-7053a0a27c2d","sourceRange":{"first":{"id":"ab071d7e-7b44-401a-b8ec-32f213c94289","sequence":14},"last":{"id":"ab071d7e-7b44-401a-b8ec-32f213c94289","sequence":14},"stream":{"id":"01a0eed7-6eb5-7490-b19e-7053a0a27c2d","kind":"session"}},"viewCursor":"v:01a0eed7-6eb5-7490-b19e-7053a0a27c2d:4"}}} +{"dir":"in","msg":{"emittedAtMs":1790713491233,"jsonrpc":"2.0","method":"item/started","params":{"item":{"childSessionId":"c775da07-e090-47c0-9f14-212a29534484","fallbackText":"Reminder child session","generationId":1,"itemId":"ec3c57ce-caae-4973-965d-93161ff55b81","kind":"reminderChild","recordedAt":"2026-09-29T20:24:51.233206Z","reminderAgentId":"skill-reminder","revision":1,"status":"inProgress","taskId":"01a0eed7-6f1f-7fc0-b779-a0458b794384","turnId":"01a0eed7-6f16-7497-ab8e-8bd0733e2f6d"},"sessionId":"01a0eed7-6eb5-7490-b19e-7053a0a27c2d","sourceRange":{"first":{"id":"ec3c57ce-caae-4973-965d-93161ff55b81","sequence":32},"last":{"id":"ec3c57ce-caae-4973-965d-93161ff55b81","sequence":32},"stream":{"id":"01a0eed7-6eb5-7490-b19e-7053a0a27c2d","kind":"session"}},"viewCursor":"v:01a0eed7-6eb5-7490-b19e-7053a0a27c2d:5"}}} +{"dir":"in","msg":{"emittedAtMs":1790713491238,"jsonrpc":"2.0","method":"session/listChanged","params":{"session":{"activeTurnId":"01a0eed7-6f16-7497-ab8e-8bd0733e2f6d","createdAt":"2026-09-29T20:24:51.128331Z","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase3/data/muse/sessions/2026/09/29/01a0eed7-6eb5-7490-b19e-7053a0a27c2d/session.jsonl","providerId":"meta","sessionId":"01a0eed7-6eb5-7490-b19e-7053a0a27c2d","status":"running","turnCount":0,"updatedAt":"2026-09-29T20:24:51.232727Z","workspaceRoot":"/tmp/muse-phase3/repo"}}}} +{"dir":"in","msg":{"emittedAtMs":1790713494252,"jsonrpc":"2.0","method":"item/started","params":{"item":{"itemId":"c5507091-bdd1-4701-be9d-1f0ab4b75b3e","kind":"agentMessage","revision":1,"status":"inProgress","text":"","turnId":"01a0eed7-6f16-7497-ab8e-8bd0733e2f6d"},"sessionId":"01a0eed7-6eb5-7490-b19e-7053a0a27c2d","viewCursor":"v:01a0eed7-6eb5-7490-b19e-7053a0a27c2d:5.1"}}} +{"dir":"in","msg":{"emittedAtMs":1790713494252,"jsonrpc":"2.0","method":"item/delta","params":{"delta":"MUSE_PARITY_USER_OK","field":"text","itemId":"c5507091-bdd1-4701-be9d-1f0ab4b75b3e","sessionId":"01a0eed7-6eb5-7490-b19e-7053a0a27c2d","viewCursor":"v:01a0eed7-6eb5-7490-b19e-7053a0a27c2d:5.2"}}} +{"dir":"in","msg":{"emittedAtMs":1790713494323,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"itemId":"c5507091-bdd1-4701-be9d-1f0ab4b75b3e","kind":"agentMessage","recordedAt":"2026-09-29T20:24:54.322754Z","revision":2,"status":"completed","text":"MUSE_PARITY_USER_OK","turnId":"01a0eed7-6f16-7497-ab8e-8bd0733e2f6d"},"sessionId":"01a0eed7-6eb5-7490-b19e-7053a0a27c2d","sourceRange":{"first":{"id":"98e5bb9a-cc37-4e94-8bc5-90ebd1bad851","sequence":45},"last":{"id":"98e5bb9a-cc37-4e94-8bc5-90ebd1bad851","sequence":45},"stream":{"id":"01a0eed7-6eb5-7490-b19e-7053a0a27c2d","kind":"session"}},"viewCursor":"v:01a0eed7-6eb5-7490-b19e-7053a0a27c2d:6"}}} +{"dir":"in","msg":{"emittedAtMs":1790713494328,"jsonrpc":"2.0","method":"session/tokenUsage","params":{"cumulative":{"outputTokens":32,"promptTokens":36717,"totalTokens":36749},"durationMs":3091,"modelId":"meta/muse-spark-1.3","promptTokens":36717,"sessionId":"01a0eed7-6eb5-7490-b19e-7053a0a27c2d","sourceRange":{"first":{"id":"475129b6-1e12-4d19-b3b8-7d8cf5ef1f71","sequence":48},"last":{"id":"475129b6-1e12-4d19-b3b8-7d8cf5ef1f71","sequence":48},"stream":{"id":"01a0eed7-6eb5-7490-b19e-7053a0a27c2d","kind":"session"}},"totalTokens":36749,"turnId":"01a0eed7-6f16-7497-ab8e-8bd0733e2f6d","usage":{"cacheReadTokens":0,"cacheWriteTokens":0,"cachedTokens":0,"inputTokens":36717,"outputTokens":32,"reasoningTokens":16},"viewCursor":"v:01a0eed7-6eb5-7490-b19e-7053a0a27c2d:7"}}} +{"dir":"in","msg":{"emittedAtMs":1790713494330,"jsonrpc":"2.0","method":"item/started","params":{"item":{"childSessionId":"230378ce-377b-4d2c-8020-10cf2449b75b","fallbackText":"Reminder child session","generationId":1,"itemId":"35da6de8-83a9-4e23-a1f9-f05c5f78103c","kind":"reminderChild","recordedAt":"2026-09-29T20:24:54.330297Z","reminderAgentId":"verify-reminder","revision":1,"status":"inProgress","taskId":"01a0eed7-7b38-71e0-b90b-3ebc7af2ad1f","turnId":"01a0eed7-6f16-7497-ab8e-8bd0733e2f6d"},"sessionId":"01a0eed7-6eb5-7490-b19e-7053a0a27c2d","sourceRange":{"first":{"id":"35da6de8-83a9-4e23-a1f9-f05c5f78103c","sequence":55},"last":{"id":"35da6de8-83a9-4e23-a1f9-f05c5f78103c","sequence":55},"stream":{"id":"01a0eed7-6eb5-7490-b19e-7053a0a27c2d","kind":"session"}},"viewCursor":"v:01a0eed7-6eb5-7490-b19e-7053a0a27c2d:8"}}} +{"dir":"in","msg":{"emittedAtMs":1790713515146,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"childSessionId":"c775da07-e090-47c0-9f14-212a29534484","fallbackText":"Reminder child session","generationId":1,"itemId":"ec3c57ce-caae-4973-965d-93161ff55b81","kind":"reminderChild","recordedAt":"2026-09-29T20:25:15.146156Z","reminderAgentId":"skill-reminder","revision":2,"status":"completed","taskId":"01a0eed7-6f1f-7fc0-b779-a0458b794384","turnId":"01a0eed7-6f16-7497-ab8e-8bd0733e2f6d"},"sessionId":"01a0eed7-6eb5-7490-b19e-7053a0a27c2d","sourceRange":{"first":{"id":"ec3c57ce-caae-4973-965d-93161ff55b81","sequence":32},"last":{"id":"d5dc841a-1273-4a41-90b4-71fb1582f087","sequence":57},"stream":{"id":"01a0eed7-6eb5-7490-b19e-7053a0a27c2d","kind":"session"}},"viewCursor":"v:01a0eed7-6eb5-7490-b19e-7053a0a27c2d:9"}}} +{"dir":"in","msg":{"emittedAtMs":1790713515147,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"childSessionId":"230378ce-377b-4d2c-8020-10cf2449b75b","fallbackText":"Reminder child session","generationId":1,"itemId":"35da6de8-83a9-4e23-a1f9-f05c5f78103c","kind":"reminderChild","recordedAt":"2026-09-29T20:25:15.146691Z","reminderAgentId":"verify-reminder","revision":2,"status":"completed","taskId":"01a0eed7-7b38-71e0-b90b-3ebc7af2ad1f","turnId":"01a0eed7-6f16-7497-ab8e-8bd0733e2f6d"},"sessionId":"01a0eed7-6eb5-7490-b19e-7053a0a27c2d","sourceRange":{"first":{"id":"35da6de8-83a9-4e23-a1f9-f05c5f78103c","sequence":55},"last":{"id":"5fe97bca-2f1c-4a08-88fe-e5ff9c313bdc","sequence":58},"stream":{"id":"01a0eed7-6eb5-7490-b19e-7053a0a27c2d","kind":"session"}},"viewCursor":"v:01a0eed7-6eb5-7490-b19e-7053a0a27c2d:10"}}} +{"dir":"in","msg":{"emittedAtMs":1790713515148,"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a0eed7-6eb5-7490-b19e-7053a0a27c2d","status":"idle","viewCursor":"v:01a0eed7-6eb5-7490-b19e-7053a0a27c2d:11"}}} +{"dir":"in","msg":{"emittedAtMs":1790713515150,"jsonrpc":"2.0","method":"turn/completed","params":{"durationMs":23923,"sessionId":"01a0eed7-6eb5-7490-b19e-7053a0a27c2d","sourceRange":{"first":{"id":"e862f004-df27-443a-95c7-2984e9702037","sequence":64},"last":{"id":"e862f004-df27-443a-95c7-2984e9702037","sequence":64},"stream":{"id":"01a0eed7-6eb5-7490-b19e-7053a0a27c2d","kind":"session"}},"terminal":"completed","timeToFirstTokenMs":3026,"turnId":"01a0eed7-6f16-7497-ab8e-8bd0733e2f6d","viewCursor":"v:01a0eed7-6eb5-7490-b19e-7053a0a27c2d:11"}}} +{"dir":"in","msg":{"emittedAtMs":1790713515150,"jsonrpc":"2.0","method":"session/listChanged","params":{"session":{"activeTurnId":null,"branch":"main","createdAt":"2026-09-29T20:24:51.125765Z","firstUserPrompt":"/muse-parity-user QA","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase3/data/muse/sessions/2026/09/29/01a0eed7-6eb5-7490-b19e-7053a0a27c2d/session.jsonl","providerId":"meta","sessionId":"01a0eed7-6eb5-7490-b19e-7053a0a27c2d","status":"idle","title":"/muse-parity-user QA","turnCount":1,"updatedAt":"2026-09-29T20:25:15.148441Z","workspaceRoot":"/tmp/muse-phase3/repo"}}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":5,"method":"usage/read","params":{}}} +{"dir":"in","msg":{"id":5,"jsonrpc":"2.0","result":{}}} +{"dir":"in","msg":{"emittedAtMs":1790713515404,"jsonrpc":"2.0","method":"session/closed","params":{"reason":"hostShutdown","sessionId":"01a0eed7-6eb5-7490-b19e-7053a0a27c2d","viewCursor":"v:01a0eed7-6eb5-7490-b19e-7053a0a27c2d:11"}}} +{"dir":"in","msg":{"emittedAtMs":1790713515404,"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a0eed7-6eb5-7490-b19e-7053a0a27c2d","status":"notLoaded","viewCursor":"v:01a0eed7-6eb5-7490-b19e-7053a0a27c2d:11"}}} +{"dir":"in","msg":{"emittedAtMs":1790713515404,"jsonrpc":"2.0","method":"session/listChanged","params":{"session":{"activeTurnId":null,"branch":"main","createdAt":"2026-09-29T20:24:51.125765Z","firstUserPrompt":"/muse-parity-user QA","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase3/data/muse/sessions/2026/09/29/01a0eed7-6eb5-7490-b19e-7053a0a27c2d/session.jsonl","providerId":"meta","sessionId":"01a0eed7-6eb5-7490-b19e-7053a0a27c2d","status":"notLoaded","title":"/muse-parity-user QA","turnCount":1,"updatedAt":"2026-09-29T20:25:15.148441Z","workspaceRoot":"/tmp/muse-phase3/repo"}}}} diff --git a/plugins/muse-provider/test/fixtures/resume-reasoning.ndjson b/plugins/muse-provider/test/fixtures/resume-reasoning.ndjson new file mode 100644 index 0000000000..548bf8ea2d --- /dev/null +++ b/plugins/muse-provider/test/fixtures/resume-reasoning.ndjson @@ -0,0 +1,27 @@ +{"dir":"out","msg":{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"clientInfo":{"name":"paseo_phase0","version":"0.0.0"},"capabilities":{"experimentalApi":true,"requestedCapabilities":["sessionMcp","sessionListStream"]}}}} +{"dir":"in","msg":{"id":1,"jsonrpc":"2.0","result":{"experimentalApi":true,"grantedCapabilities":["sessionMcp","sessionListStream"],"museHome":"/tmp/muse-phase0/data/muse","platformFamily":"unix","platformOs":"linux","schema":{"fingerprint":"sha256:e0e163db6ccf00dbe68402ce55d6319b3edc33c421f31e9583b587b2de8a118f","version":1},"serverInfo":{"name":"muse","version":"1.4.1"},"sessionDurability":"durable","userAgent":"muse-build/1.4.1 (non-interactive; linux-x86_64; build 35815c253477406321e3f5595195becc25e3907a)"}}} +{"dir":"out","msg":{"jsonrpc":"2.0","method":"initialized","params":{}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":2,"method":"session/resume","params":{"commandId":"01a0ee4c-4bb3-7e83-ad09-930d246e0d31","sessionId":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c"}}} +{"dir":"in","msg":{"emittedAtMs":1790704372759,"jsonrpc":"2.0","method":"session/branchChanged","params":{"branch":"main","sessionId":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","sourceRange":{"first":{"id":"c262c112-2245-4975-a0d3-de21a8f19459","sequence":166},"last":{"id":"c262c112-2245-4975-a0d3-de21a8f19459","sequence":166},"stream":{"id":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","kind":"session"}},"vcs":"git","viewCursor":"v:01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c:26","workspaceRoot":"/tmp/muse-phase0/repo"}}} +{"dir":"in","msg":{"id":2,"jsonrpc":"2.0","result":{"history":{"items":[{"commandId":"01a0ee46-ea2b-72e5-aada-1408802a3242","itemId":"bee9b543-239a-4c82-96c7-c482cf3c23ff","kind":"userMessage","recordedAt":"2026-09-29T17:47:00.014911Z","revision":1,"status":"completed","text":"Run bash command sleep 12; then respond ORIGINAL_DONE. Do not respond before the command finishes.","turnId":"01a0ee46-ea2b-72e5-aada-1408802a3242"},{"childSessionId":"dfb4300f-9488-4b4e-99a5-7b29d322952d","fallbackText":"Reminder child session","generationId":1,"itemId":"25799bf7-f24a-4eed-9953-f39079152e48","kind":"reminderChild","recordedAt":"2026-09-29T17:47:13.76483Z","reminderAgentId":"skill-reminder","revision":2,"status":"completed","taskId":"01a0ee46-ea35-7300-9f57-faffb63ca778","turnId":"01a0ee46-ea2b-72e5-aada-1408802a3242"},{"args":"{\"command\":\"sleep 12\",\"description\":\"Sleep for twelve seconds\",\"yield_time_ms\":15000}","background":true,"backgroundInitiator":"timeout","callId":"call_01a0ee46f61c7482b289e8bb6aee0f02","itemId":"01a0ee46-f8c9-7db3-ac02-f2b78cfc7c56","kind":"toolCall","recordedAt":"2026-09-29T17:47:15.961748Z","revision":3,"status":"completed","tool":"bash","turnId":"01a0ee46-ea2b-72e5-aada-1408802a3242","visibleOutput":"{\n \"chunk_id\": \"exec-1-1\",\n \"command\": \"sleep 12\",\n \"description\": \"Sleep for twelve seconds\",\n \"execution_state\": \"background_running\",\n \"session_id\": 1,\n \"work_id\": \"work.v1.managed_bash.sha256.10c46069f6af08edd1e9a61d87aea3c93ab0bf6df4d8fe5d5ab2efe16c2aa63e\",\n \"model_guidance\": \"The command is still running in the background and the UI already shows its status. Do not poll for completion: the final output is delivered to you automatically as runtime context and wakes you even after you end the turn. Exception: when a runtime overdue notice names a still-running session, you may inspect it or terminate it with bash_input now. Do not narrate that it started, is running, shows current output, will continue, or has an internal session_id unless the user explicitly asks or you have substantive next work. If there is nothing else to do, end the turn without extra status text. Managed sessions end when your agent session ends: a deliverable process that must keep running after your final answer needs a detached `setsid -f` start instead (where setsid exists \u2014 Linux).\",\n \"output\": \"\",\n \"original_output_bytes\": 0,\n \"original_output_tokens\": 0,\n \"truncated\": false\n}"},{"commandId":"01a0ee46-fcb3-78ab-a15d-29882a94f6d1","itemId":"591cef15-1dd4-5a6d-ad43-d21def93699b","kind":"userMessage","recordedAt":"2026-09-29T17:47:13.762324Z","revision":1,"status":"completed","steered":true,"text":"Change the final reply to STEERED_PHASE0_OK and do not say ORIGINAL_DONE.","turnId":"01a0ee46-ea2b-72e5-aada-1408802a3242"},{"childSessionId":"4ba6a4a2-695b-4e19-b93f-399028c26c85","fallbackText":"Reminder child session","generationId":2,"itemId":"e2deeff1-ddc1-46c5-b053-fdc0c7a12ad2","kind":"reminderChild","recordedAt":"2026-09-29T17:47:40.516146Z","reminderAgentId":"skill-reminder","revision":2,"status":"completed","taskId":"01a0ee47-1fe5-72a0-84bd-4c9e9446ed0d","turnId":"01a0ee46-ea2b-72e5-aada-1408802a3242"},{"itemId":"f802477d-5ebb-4a3d-b573-1624f746f95d","kind":"reasoning","providerItemId":"rs_6abbf9a31753fbc1f4c04388:rs_01a0ee47259e73e49456486349db6e0e","recordedAt":"2026-09-29T17:47:40.272906Z","revision":2,"status":"completed","summary":["Evaluating conflicting instructions and stop-signal guidance while waiting for the background command to finish.Resolving instruction precedence while deferring the reply until the background command completes.Deferring the final reply while waiting for the background command to complete."],"turnId":"01a0ee46-ea2b-72e5-aada-1408802a3242"},{"args":"{}","callId":"call_01a0ee4786c270bdb312bdd0ddf75a64","itemId":"01a0ee47-885e-7440-9a65-b3f7d88924a2","kind":"toolCall","recordedAt":"2026-09-29T17:47:40.51315Z","revision":2,"status":"completed","tool":"work_list","turnId":"01a0ee46-ea2b-72e5-aada-1408802a3242","visibleOutput":"{\"items\":[],\"next_after_work_id\":null}"},{"childSessionId":"c03ed820-1aea-441c-8b83-c56e0a21f749","fallbackText":"Reminder child session","generationId":3,"itemId":"6873949c-68f0-43f2-90d8-ffbe78d1d4e6","kind":"reminderChild","recordedAt":"2026-09-29T17:47:50.732258Z","reminderAgentId":"skill-reminder","revision":2,"status":"completed","taskId":"01a0ee47-8865-7b53-b394-8d0a49863634","turnId":"01a0ee46-ea2b-72e5-aada-1408802a3242"},{"itemId":"b7f0225d-5758-4365-987d-4931bba57bb2","kind":"agentMessage","recordedAt":"2026-09-29T17:47:45.116004Z","revision":2,"status":"completed","text":"STEERED_PHASE0_OK","turnId":"01a0ee46-ea2b-72e5-aada-1408802a3242"},{"childSessionId":"61d34ee6-0447-4424-b6f6-6606ed0465c2","childSessionLogPath":"subagent/61d34ee6-0447-4424-b6f6-6606ed0465c2/session.jsonl","fallbackText":"Reminder child session","generationId":1,"itemId":"411ee9ec-dfbd-4d09-ab8d-1b25ee41a63d","kind":"reminderChild","recordedAt":"2026-09-29T17:47:50.732876Z","reminderAgentId":"verify-reminder","revision":2,"status":"completed","taskId":"01a0ee47-9a61-72d2-887f-7286aa2c2eb5","turnId":"01a0ee46-ea2b-72e5-aada-1408802a3242"}],"mode":"inline","snapshot":null},"pendingRequests":[],"session":{"activeTurnId":null,"approvalMode":{"lastCommandId":null,"mode":"onRequest","source":"replay"},"branch":"main","createdAt":"2026-09-29T17:46:59.924858Z","firstUserPrompt":"Run bash command sleep 12; then respond ORIGINAL_DONE. Do not respond before the command finishes.","forkedFrom":null,"lastActivityAt":"2026-09-29T17:47:50.73531Z","modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c/session.jsonl","providerId":"meta","sessionId":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","status":"idle","title":"Run bash command sleep 12; then respond ORIGINAL_DONE. Do not respond before the command finishes.","turnCount":1,"updatedAt":"2026-09-29T17:52:52.675916Z","workspaceRoot":"/tmp/muse-phase0/repo"},"viewCursor":"v:01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c:26"}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":3,"method":"turn/start","params":{"commandId":"01a0ee4c-4c1f-79fb-a26a-4f18cbcf2827","sessionId":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","input":[{"type":"text","text":"Reply exactly RESUMED_REASONING_OK. Do not use tools."}],"reasoningEffort":"low"}}} +{"dir":"in","msg":{"emittedAtMs":1790704372768,"jsonrpc":"2.0","method":"session/branchChanged","params":{"branch":"main","sessionId":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","sourceRange":{"first":{"id":"8d9c2ef7-22ea-4bf5-9135-c58fc18e9f2e","sequence":172},"last":{"id":"8d9c2ef7-22ea-4bf5-9135-c58fc18e9f2e","sequence":172},"stream":{"id":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","kind":"session"}},"vcs":"git","viewCursor":"v:01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c:27","workspaceRoot":"/tmp/muse-phase0/repo"}}} +{"dir":"in","msg":{"id":3,"jsonrpc":"2.0","result":{"commandId":"01a0ee4c-4c1f-79fb-a26a-4f18cbcf2827","disposition":"started","startedNewTurn":true,"status":"accepted","turnId":"01a0ee4c-4c1f-79fb-a26a-4f18cbcf2827"}}} +{"dir":"in","msg":{"emittedAtMs":1790704372776,"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","status":"running","viewCursor":"v:01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c:28"}}} +{"dir":"in","msg":{"emittedAtMs":1790704372777,"jsonrpc":"2.0","method":"turn/started","params":{"commandId":"01a0ee4c-4c1f-79fb-a26a-4f18cbcf2827","sessionId":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","sourceRange":{"first":{"id":"9daff9ed-dd93-4ce5-85a0-f1072bf5493a","sequence":174},"last":{"id":"9daff9ed-dd93-4ce5-85a0-f1072bf5493a","sequence":174},"stream":{"id":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","kind":"session"}},"turnId":"01a0ee4c-4c1f-79fb-a26a-4f18cbcf2827","viewCursor":"v:01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c:28"}}} +{"dir":"in","msg":{"emittedAtMs":1790704372777,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"commandId":"01a0ee4c-4c1f-79fb-a26a-4f18cbcf2827","itemId":"ee63b441-2b31-4c36-b32b-c171b72a70f8","kind":"userMessage","recordedAt":"2026-09-29T17:52:52.76929Z","revision":1,"status":"completed","text":"Reply exactly RESUMED_REASONING_OK. Do not use tools.","turnId":"01a0ee4c-4c1f-79fb-a26a-4f18cbcf2827"},"sessionId":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","sourceRange":{"first":{"id":"9daff9ed-dd93-4ce5-85a0-f1072bf5493a","sequence":174},"last":{"id":"9daff9ed-dd93-4ce5-85a0-f1072bf5493a","sequence":174},"stream":{"id":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","kind":"session"}},"viewCursor":"v:01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c:29"}}} +{"dir":"in","msg":{"emittedAtMs":1790704372777,"jsonrpc":"2.0","method":"item/started","params":{"item":{"childSessionId":"f5a6a72e-b5f6-4ea6-9145-99fcf846a384","fallbackText":"Reminder child session","generationId":1,"itemId":"3c846322-d54d-4b29-adcf-297cae49bd3b","kind":"reminderChild","recordedAt":"2026-09-29T17:52:52.774658Z","reminderAgentId":"skill-reminder","revision":1,"status":"inProgress","taskId":"01a0ee4c-4c26-7ec2-b0f7-97d33cd212d2","turnId":"01a0ee4c-4c1f-79fb-a26a-4f18cbcf2827"},"sessionId":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","sourceRange":{"first":{"id":"3c846322-d54d-4b29-adcf-297cae49bd3b","sequence":189},"last":{"id":"3c846322-d54d-4b29-adcf-297cae49bd3b","sequence":189},"stream":{"id":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","kind":"session"}},"viewCursor":"v:01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c:30"}}} +{"dir":"in","msg":{"emittedAtMs":1790704372777,"jsonrpc":"2.0","method":"session/listChanged","params":{"session":{"activeTurnId":"01a0ee4c-4c1f-79fb-a26a-4f18cbcf2827","branch":"main","createdAt":"2026-09-29T17:46:59.924858Z","firstUserPrompt":"Run bash command sleep 12; then respond ORIGINAL_DONE. Do not respond before the command finishes.","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c/session.jsonl","providerId":"meta","sessionId":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","status":"running","title":"Run bash command sleep 12; then respond ORIGINAL_DONE. Do not respond before the command finishes.","turnCount":1,"updatedAt":"2026-09-29T17:52:52.776079Z","workspaceRoot":"/tmp/muse-phase0/repo"}}}} +{"dir":"in","msg":{"emittedAtMs":1790704375471,"jsonrpc":"2.0","method":"item/started","params":{"item":{"itemId":"8f6cd252-a989-4ae1-ad3a-0a3bd6641c7d","kind":"agentMessage","revision":1,"status":"inProgress","text":"","turnId":"01a0ee4c-4c1f-79fb-a26a-4f18cbcf2827"},"sessionId":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","viewCursor":"v:01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c:30.1"}}} +{"dir":"in","msg":{"emittedAtMs":1790704375471,"jsonrpc":"2.0","method":"item/delta","params":{"delta":"RESUMED_REASONING_OK","field":"text","itemId":"8f6cd252-a989-4ae1-ad3a-0a3bd6641c7d","sessionId":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","viewCursor":"v:01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c:30.2"}}} +{"dir":"in","msg":{"emittedAtMs":1790704375471,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"itemId":"8f6cd252-a989-4ae1-ad3a-0a3bd6641c7d","kind":"agentMessage","recordedAt":"2026-09-29T17:52:55.471707Z","revision":2,"status":"completed","text":"RESUMED_REASONING_OK","turnId":"01a0ee4c-4c1f-79fb-a26a-4f18cbcf2827"},"sessionId":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","sourceRange":{"first":{"id":"6d43268c-d4ee-4d95-945c-eb14edbca40f","sequence":202},"last":{"id":"6d43268c-d4ee-4d95-945c-eb14edbca40f","sequence":202},"stream":{"id":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","kind":"session"}},"viewCursor":"v:01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c:31"}}} +{"dir":"in","msg":{"emittedAtMs":1790704375477,"jsonrpc":"2.0","method":"session/tokenUsage","params":{"cumulative":{"outputTokens":1826,"promptTokens":155365,"totalTokens":157191},"durationMs":2700,"modelId":"meta/muse-spark-1.3","promptTokens":38087,"sessionId":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","sourceRange":{"first":{"id":"e738e874-b4fa-41c7-97a0-78035ef44398","sequence":205},"last":{"id":"e738e874-b4fa-41c7-97a0-78035ef44398","sequence":205},"stream":{"id":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","kind":"session"}},"totalTokens":38119,"turnId":"01a0ee4c-4c1f-79fb-a26a-4f18cbcf2827","usage":{"cacheReadTokens":0,"cacheWriteTokens":0,"cachedTokens":0,"inputTokens":38087,"outputTokens":32,"reasoningTokens":15},"viewCursor":"v:01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c:32"}}} +{"dir":"in","msg":{"emittedAtMs":1790704375477,"jsonrpc":"2.0","method":"item/started","params":{"item":{"childSessionId":"80e453a6-e7d8-4f68-be48-f9247eb569c4","fallbackText":"Reminder child session","generationId":1,"itemId":"d94cbdd9-6923-4c0b-ab29-7204b1311944","kind":"reminderChild","recordedAt":"2026-09-29T17:52:55.477459Z","reminderAgentId":"verify-reminder","revision":1,"status":"inProgress","taskId":"01a0ee4c-56b5-71e3-8670-cc1c134ef4c1","turnId":"01a0ee4c-4c1f-79fb-a26a-4f18cbcf2827"},"sessionId":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","sourceRange":{"first":{"id":"d94cbdd9-6923-4c0b-ab29-7204b1311944","sequence":212},"last":{"id":"d94cbdd9-6923-4c0b-ab29-7204b1311944","sequence":212},"stream":{"id":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","kind":"session"}},"viewCursor":"v:01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c:33"}}} +{"dir":"in","msg":{"emittedAtMs":1790704381132,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"childSessionId":"f5a6a72e-b5f6-4ea6-9145-99fcf846a384","fallbackText":"Reminder child session","generationId":1,"itemId":"3c846322-d54d-4b29-adcf-297cae49bd3b","kind":"reminderChild","recordedAt":"2026-09-29T17:53:01.131905Z","reminderAgentId":"skill-reminder","revision":2,"status":"completed","taskId":"01a0ee4c-4c26-7ec2-b0f7-97d33cd212d2","turnId":"01a0ee4c-4c1f-79fb-a26a-4f18cbcf2827"},"sessionId":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","sourceRange":{"first":{"id":"3c846322-d54d-4b29-adcf-297cae49bd3b","sequence":189},"last":{"id":"9119fc4f-bbad-4f4b-b598-e6dec79b58e4","sequence":214},"stream":{"id":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","kind":"session"}},"viewCursor":"v:01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c:34"}}} +{"dir":"in","msg":{"emittedAtMs":1790704381132,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"childSessionId":"80e453a6-e7d8-4f68-be48-f9247eb569c4","fallbackText":"Reminder child session","generationId":1,"itemId":"d94cbdd9-6923-4c0b-ab29-7204b1311944","kind":"reminderChild","recordedAt":"2026-09-29T17:53:01.132067Z","reminderAgentId":"verify-reminder","revision":2,"status":"completed","taskId":"01a0ee4c-56b5-71e3-8670-cc1c134ef4c1","turnId":"01a0ee4c-4c1f-79fb-a26a-4f18cbcf2827"},"sessionId":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","sourceRange":{"first":{"id":"d94cbdd9-6923-4c0b-ab29-7204b1311944","sequence":212},"last":{"id":"8c77fcc2-590b-4357-9362-06eb4d178210","sequence":215},"stream":{"id":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","kind":"session"}},"viewCursor":"v:01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c:35"}}} +{"dir":"in","msg":{"emittedAtMs":1790704381132,"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","status":"idle","viewCursor":"v:01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c:36"}}} +{"dir":"in","msg":{"emittedAtMs":1790704381133,"jsonrpc":"2.0","method":"turn/completed","params":{"durationMs":8363,"sessionId":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","sourceRange":{"first":{"id":"cbd0c517-6488-4cad-8943-00f10f14ad1e","sequence":221},"last":{"id":"cbd0c517-6488-4cad-8943-00f10f14ad1e","sequence":221},"stream":{"id":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","kind":"session"}},"terminal":"completed","timeToFirstTokenMs":2702,"turnId":"01a0ee4c-4c1f-79fb-a26a-4f18cbcf2827","viewCursor":"v:01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c:36"}}} +{"dir":"in","msg":{"emittedAtMs":1790704381134,"jsonrpc":"2.0","method":"session/listChanged","params":{"session":{"activeTurnId":null,"branch":"main","createdAt":"2026-09-29T17:46:59.924858Z","firstUserPrompt":"Run bash command sleep 12; then respond ORIGINAL_DONE. Do not respond before the command finishes.","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c/session.jsonl","providerId":"meta","sessionId":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","status":"idle","title":"Run bash command sleep 12; then respond ORIGINAL_DONE. Do not respond before the command finishes.","turnCount":2,"updatedAt":"2026-09-29T17:53:01.132363Z","workspaceRoot":"/tmp/muse-phase0/repo"}}}} +{"dir":"in","msg":{"emittedAtMs":1790704381377,"jsonrpc":"2.0","method":"session/closed","params":{"reason":"hostShutdown","sessionId":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","viewCursor":"v:01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c:37"}}} +{"dir":"in","msg":{"emittedAtMs":1790704381377,"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","status":"notLoaded","viewCursor":"v:01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c:37"}}} +{"dir":"in","msg":{"emittedAtMs":1790704381377,"jsonrpc":"2.0","method":"session/listChanged","params":{"session":{"activeTurnId":null,"branch":"main","createdAt":"2026-09-29T17:46:59.924858Z","firstUserPrompt":"Run bash command sleep 12; then respond ORIGINAL_DONE. Do not respond before the command finishes.","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c/session.jsonl","providerId":"meta","sessionId":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","status":"notLoaded","title":"Run bash command sleep 12; then respond ORIGINAL_DONE. Do not respond before the command finishes.","turnCount":2,"updatedAt":"2026-09-29T17:53:01.133883Z","workspaceRoot":"/tmp/muse-phase0/repo"}}}} diff --git a/plugins/muse-provider/test/fixtures/resume-source.ndjson b/plugins/muse-provider/test/fixtures/resume-source.ndjson new file mode 100644 index 0000000000..8676d15e45 --- /dev/null +++ b/plugins/muse-provider/test/fixtures/resume-source.ndjson @@ -0,0 +1,26 @@ +{"dir":"out","msg":{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"clientInfo":{"name":"paseo_phase0","version":"0.0.0"},"capabilities":{"experimentalApi":true,"requestedCapabilities":["sessionMcp","sessionListStream"]}}}} +{"dir":"in","msg":{"id":1,"jsonrpc":"2.0","result":{"experimentalApi":true,"grantedCapabilities":["sessionMcp","sessionListStream"],"museHome":"/tmp/muse-phase0/data/muse","platformFamily":"unix","platformOs":"linux","schema":{"fingerprint":"sha256:e0e163db6ccf00dbe68402ce55d6319b3edc33c421f31e9583b587b2de8a118f","version":1},"serverInfo":{"name":"muse","version":"1.4.1"},"sessionDurability":"durable","userAgent":"muse-build/1.4.1 (non-interactive; linux-x86_64; build 35815c253477406321e3f5595195becc25e3907a)"}}} +{"dir":"out","msg":{"jsonrpc":"2.0","method":"initialized","params":{}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":2,"method":"session/start","params":{"commandId":"01a0ee48-370c-77f6-a1d7-ea885b8777bf","workspaceRoot":"/tmp/muse-phase0/repo","modelId":"meta/muse-spark-1.3","providerId":"meta","approvalMode":"onRequest"}}} +{"dir":"in","msg":{"emittedAtMs":1790704105319,"jsonrpc":"2.0","method":"session/started","params":{"session":{"activeTurnId":null,"approvalMode":{"lastCommandId":null,"mode":"onRequest","source":"startup"},"createdAt":"2026-09-29T17:48:25.238493Z","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee48-3713-70a0-bab7-ab463b7bc580/session.jsonl","providerId":"meta","sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","status":"idle","turnCount":0,"updatedAt":"2026-09-29T17:48:25.238497Z","workspaceRoot":"/tmp/muse-phase0/repo"}}}} +{"dir":"in","msg":{"id":2,"jsonrpc":"2.0","result":{"session":{"activeTurnId":null,"approvalMode":{"lastCommandId":null,"mode":"onRequest","source":"startup"},"createdAt":"2026-09-29T17:48:25.238493Z","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee48-3713-70a0-bab7-ab463b7bc580/session.jsonl","providerId":"meta","sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","status":"idle","turnCount":0,"updatedAt":"2026-09-29T17:48:25.238497Z","workspaceRoot":"/tmp/muse-phase0/repo"},"viewCursor":"v:01a0ee48-3713-70a0-bab7-ab463b7bc580:1"}}} +{"dir":"in","msg":{"emittedAtMs":1790704105325,"jsonrpc":"2.0","method":"session/approvalModeChanged","params":{"clientName":"paseo_phase0","commandId":"01a0ee48-370c-77f6-a1d7-ea885b8777bf","mode":"onRequest","sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","source":"approvalReconfigure","sourceRange":{"first":{"id":"1114a956-7136-4272-8928-65125b088788","sequence":5},"last":{"id":"1114a956-7136-4272-8928-65125b088788","sequence":5},"stream":{"id":"01a0ee48-3713-70a0-bab7-ab463b7bc580","kind":"session"}},"viewCursor":"v:01a0ee48-3713-70a0-bab7-ab463b7bc580:1"}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":3,"method":"turn/start","params":{"commandId":"01a0ee48-376d-7cff-a66c-5e1ac7035295","sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","input":[{"type":"text","text":"Reply exactly RESUME_MARKER_OK without tools."}],"reasoningEffort":"low"}}} +{"dir":"in","msg":{"emittedAtMs":1790704105327,"jsonrpc":"2.0","method":"session/branchChanged","params":{"branch":"main","sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","sourceRange":{"first":{"id":"186af8df-b20d-481a-bdf5-592206b9239f","sequence":15},"last":{"id":"186af8df-b20d-481a-bdf5-592206b9239f","sequence":15},"stream":{"id":"01a0ee48-3713-70a0-bab7-ab463b7bc580","kind":"session"}},"vcs":"git","viewCursor":"v:01a0ee48-3713-70a0-bab7-ab463b7bc580:2","workspaceRoot":"/tmp/muse-phase0/repo"}}} +{"dir":"in","msg":{"id":3,"jsonrpc":"2.0","result":{"commandId":"01a0ee48-376d-7cff-a66c-5e1ac7035295","disposition":"started","startedNewTurn":true,"status":"accepted","turnId":"01a0ee48-376d-7cff-a66c-5e1ac7035295"}}} +{"dir":"in","msg":{"emittedAtMs":1790704105335,"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","status":"running","viewCursor":"v:01a0ee48-3713-70a0-bab7-ab463b7bc580:3"}}} +{"dir":"in","msg":{"emittedAtMs":1790704105336,"jsonrpc":"2.0","method":"turn/started","params":{"commandId":"01a0ee48-376d-7cff-a66c-5e1ac7035295","sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","sourceRange":{"first":{"id":"3e854976-f69f-406b-8f8d-783610940ff2","sequence":17},"last":{"id":"3e854976-f69f-406b-8f8d-783610940ff2","sequence":17},"stream":{"id":"01a0ee48-3713-70a0-bab7-ab463b7bc580","kind":"session"}},"turnId":"01a0ee48-376d-7cff-a66c-5e1ac7035295","viewCursor":"v:01a0ee48-3713-70a0-bab7-ab463b7bc580:3"}}} +{"dir":"in","msg":{"emittedAtMs":1790704105336,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"commandId":"01a0ee48-376d-7cff-a66c-5e1ac7035295","itemId":"21b34e05-d5c5-492b-b166-b261039b69b1","kind":"userMessage","recordedAt":"2026-09-29T17:48:25.328624Z","revision":1,"status":"completed","text":"Reply exactly RESUME_MARKER_OK without tools.","turnId":"01a0ee48-376d-7cff-a66c-5e1ac7035295"},"sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","sourceRange":{"first":{"id":"3e854976-f69f-406b-8f8d-783610940ff2","sequence":17},"last":{"id":"3e854976-f69f-406b-8f8d-783610940ff2","sequence":17},"stream":{"id":"01a0ee48-3713-70a0-bab7-ab463b7bc580","kind":"session"}},"viewCursor":"v:01a0ee48-3713-70a0-bab7-ab463b7bc580:4"}}} +{"dir":"in","msg":{"emittedAtMs":1790704105336,"jsonrpc":"2.0","method":"item/started","params":{"item":{"childSessionId":"76293abd-9c8a-4a09-8edf-377e9e17bf81","fallbackText":"Reminder child session","generationId":1,"itemId":"1275560b-0426-4de0-91c2-e76c1698e73b","kind":"reminderChild","recordedAt":"2026-09-29T17:48:25.335588Z","reminderAgentId":"skill-reminder","revision":1,"status":"inProgress","taskId":"01a0ee48-3776-7a82-b1bc-b843d01dbbf3","turnId":"01a0ee48-376d-7cff-a66c-5e1ac7035295"},"sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","sourceRange":{"first":{"id":"1275560b-0426-4de0-91c2-e76c1698e73b","sequence":35},"last":{"id":"1275560b-0426-4de0-91c2-e76c1698e73b","sequence":35},"stream":{"id":"01a0ee48-3713-70a0-bab7-ab463b7bc580","kind":"session"}},"viewCursor":"v:01a0ee48-3713-70a0-bab7-ab463b7bc580:5"}}} +{"dir":"in","msg":{"emittedAtMs":1790704105340,"jsonrpc":"2.0","method":"session/listChanged","params":{"session":{"activeTurnId":"01a0ee48-376d-7cff-a66c-5e1ac7035295","createdAt":"2026-09-29T17:48:25.238493Z","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee48-3713-70a0-bab7-ab463b7bc580/session.jsonl","providerId":"meta","sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","status":"running","turnCount":0,"updatedAt":"2026-09-29T17:48:25.335835Z","workspaceRoot":"/tmp/muse-phase0/repo"}}}} +{"dir":"in","msg":{"emittedAtMs":1790704108467,"jsonrpc":"2.0","method":"item/started","params":{"item":{"itemId":"141a95ec-0b8f-47fe-9e95-c3cfcc50b8a5","kind":"agentMessage","revision":1,"status":"inProgress","text":"","turnId":"01a0ee48-376d-7cff-a66c-5e1ac7035295"},"sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","viewCursor":"v:01a0ee48-3713-70a0-bab7-ab463b7bc580:5.1"}}} +{"dir":"in","msg":{"emittedAtMs":1790704108467,"jsonrpc":"2.0","method":"item/delta","params":{"delta":"RESUME_MARKER_OK","field":"text","itemId":"141a95ec-0b8f-47fe-9e95-c3cfcc50b8a5","sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","viewCursor":"v:01a0ee48-3713-70a0-bab7-ab463b7bc580:5.2"}}} +{"dir":"in","msg":{"emittedAtMs":1790704108508,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"itemId":"141a95ec-0b8f-47fe-9e95-c3cfcc50b8a5","kind":"agentMessage","recordedAt":"2026-09-29T17:48:28.508252Z","revision":2,"status":"completed","text":"RESUME_MARKER_OK","turnId":"01a0ee48-376d-7cff-a66c-5e1ac7035295"},"sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","sourceRange":{"first":{"id":"4dd2a6ba-ca4e-43f0-a652-abe5fa688f28","sequence":48},"last":{"id":"4dd2a6ba-ca4e-43f0-a652-abe5fa688f28","sequence":48},"stream":{"id":"01a0ee48-3713-70a0-bab7-ab463b7bc580","kind":"session"}},"viewCursor":"v:01a0ee48-3713-70a0-bab7-ab463b7bc580:6"}}} +{"dir":"in","msg":{"emittedAtMs":1790704108518,"jsonrpc":"2.0","method":"session/tokenUsage","params":{"cumulative":{"outputTokens":27,"promptTokens":38130,"totalTokens":38157},"durationMs":3178,"modelId":"meta/muse-spark-1.3","promptTokens":38130,"sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","sourceRange":{"first":{"id":"95501ca7-8eb6-4e73-9b15-33a440a26899","sequence":51},"last":{"id":"95501ca7-8eb6-4e73-9b15-33a440a26899","sequence":51},"stream":{"id":"01a0ee48-3713-70a0-bab7-ab463b7bc580","kind":"session"}},"totalTokens":38157,"turnId":"01a0ee48-376d-7cff-a66c-5e1ac7035295","usage":{"cacheReadTokens":8433,"cacheWriteTokens":0,"cachedTokens":8433,"inputTokens":38130,"outputTokens":27,"reasoningTokens":12},"viewCursor":"v:01a0ee48-3713-70a0-bab7-ab463b7bc580:7"}}} +{"dir":"in","msg":{"emittedAtMs":1790704108520,"jsonrpc":"2.0","method":"item/started","params":{"item":{"childSessionId":"e9f3e28f-f760-4a06-8127-543e89e5ca72","childSessionLogPath":"subagent/e9f3e28f-f760-4a06-8127-543e89e5ca72/session.jsonl","fallbackText":"Reminder child session","generationId":1,"itemId":"bebe2029-2f83-4cbb-9ac5-6da291835a0c","kind":"reminderChild","recordedAt":"2026-09-29T17:48:28.520288Z","reminderAgentId":"verify-reminder","revision":1,"status":"inProgress","taskId":"01a0ee48-43e7-7921-8a10-bbfc1f118cdc","turnId":"01a0ee48-376d-7cff-a66c-5e1ac7035295"},"sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","sourceRange":{"first":{"id":"bebe2029-2f83-4cbb-9ac5-6da291835a0c","sequence":58},"last":{"id":"bebe2029-2f83-4cbb-9ac5-6da291835a0c","sequence":58},"stream":{"id":"01a0ee48-3713-70a0-bab7-ab463b7bc580","kind":"session"}},"viewCursor":"v:01a0ee48-3713-70a0-bab7-ab463b7bc580:8"}}} +{"dir":"in","msg":{"emittedAtMs":1790704110815,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"childSessionId":"e9f3e28f-f760-4a06-8127-543e89e5ca72","childSessionLogPath":"subagent/e9f3e28f-f760-4a06-8127-543e89e5ca72/session.jsonl","fallbackText":"Reminder child session","generationId":1,"itemId":"bebe2029-2f83-4cbb-9ac5-6da291835a0c","kind":"reminderChild","recordedAt":"2026-09-29T17:48:30.814728Z","reminderAgentId":"verify-reminder","revision":2,"status":"completed","taskId":"01a0ee48-43e7-7921-8a10-bbfc1f118cdc","turnId":"01a0ee48-376d-7cff-a66c-5e1ac7035295"},"sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","sourceRange":{"first":{"id":"bebe2029-2f83-4cbb-9ac5-6da291835a0c","sequence":58},"last":{"id":"89cebb28-940c-4f03-975b-08a31fb6130b","sequence":60},"stream":{"id":"01a0ee48-3713-70a0-bab7-ab463b7bc580","kind":"session"}},"viewCursor":"v:01a0ee48-3713-70a0-bab7-ab463b7bc580:9"}}} +{"dir":"in","msg":{"emittedAtMs":1790704110818,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"childSessionId":"76293abd-9c8a-4a09-8edf-377e9e17bf81","failureReason":"main run completed","fallbackText":"Reminder child session","generationId":1,"itemId":"1275560b-0426-4de0-91c2-e76c1698e73b","kind":"reminderChild","recordedAt":"2026-09-29T17:48:30.818129Z","reminderAgentId":"skill-reminder","revision":2,"status":"cancelled","taskId":"01a0ee48-3776-7a82-b1bc-b843d01dbbf3","turnId":"01a0ee48-376d-7cff-a66c-5e1ac7035295"},"sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","sourceRange":{"first":{"id":"1275560b-0426-4de0-91c2-e76c1698e73b","sequence":35},"last":{"id":"d87d7ad7-417f-4517-bcd2-c9df575e485d","sequence":64},"stream":{"id":"01a0ee48-3713-70a0-bab7-ab463b7bc580","kind":"session"}},"viewCursor":"v:01a0ee48-3713-70a0-bab7-ab463b7bc580:10"}}} +{"dir":"in","msg":{"emittedAtMs":1790704110818,"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","status":"idle","viewCursor":"v:01a0ee48-3713-70a0-bab7-ab463b7bc580:11"}}} +{"dir":"in","msg":{"emittedAtMs":1790704110819,"jsonrpc":"2.0","method":"turn/completed","params":{"durationMs":5489,"sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","sourceRange":{"first":{"id":"a16b76fe-60d7-4208-9c2c-d74d1f03e264","sequence":66},"last":{"id":"a16b76fe-60d7-4208-9c2c-d74d1f03e264","sequence":66},"stream":{"id":"01a0ee48-3713-70a0-bab7-ab463b7bc580","kind":"session"}},"terminal":"completed","timeToFirstTokenMs":3139,"turnId":"01a0ee48-376d-7cff-a66c-5e1ac7035295","viewCursor":"v:01a0ee48-3713-70a0-bab7-ab463b7bc580:11"}}} +{"dir":"in","msg":{"emittedAtMs":1790704110819,"jsonrpc":"2.0","method":"session/branchChanged","params":{"branch":"main","sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","sourceRange":{"first":{"id":"d1b814a6-7e5a-4dab-8018-97c1116890e6","sequence":67},"last":{"id":"d1b814a6-7e5a-4dab-8018-97c1116890e6","sequence":67},"stream":{"id":"01a0ee48-3713-70a0-bab7-ab463b7bc580","kind":"session"}},"vcs":"git","viewCursor":"v:01a0ee48-3713-70a0-bab7-ab463b7bc580:12","workspaceRoot":"/tmp/muse-phase0/repo"}}} +{"dir":"in","msg":{"emittedAtMs":1790704110819,"jsonrpc":"2.0","method":"session/listChanged","params":{"session":{"activeTurnId":null,"branch":"main","createdAt":"2026-09-29T17:48:25.236035Z","firstUserPrompt":"Reply exactly RESUME_MARKER_OK without tools.","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee48-3713-70a0-bab7-ab463b7bc580/session.jsonl","providerId":"meta","sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","status":"idle","title":"Reply exactly RESUME_MARKER_OK without tools.","turnCount":1,"updatedAt":"2026-09-29T17:48:30.819204Z","workspaceRoot":"/tmp/muse-phase0/repo"}}}} diff --git a/plugins/muse-provider/test/fixtures/resume-with-cursor.ndjson b/plugins/muse-provider/test/fixtures/resume-with-cursor.ndjson new file mode 100644 index 0000000000..a11c4d0aba --- /dev/null +++ b/plugins/muse-provider/test/fixtures/resume-with-cursor.ndjson @@ -0,0 +1,40 @@ +{"dir":"out","msg":{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"clientInfo":{"name":"paseo_phase0","version":"0.0.0"},"capabilities":{"experimentalApi":true,"requestedCapabilities":["sessionMcp","sessionListStream"]}}}} +{"dir":"in","msg":{"id":1,"jsonrpc":"2.0","result":{"experimentalApi":true,"grantedCapabilities":["sessionMcp","sessionListStream"],"museHome":"/tmp/muse-phase0/data/muse","platformFamily":"unix","platformOs":"linux","schema":{"fingerprint":"sha256:e0e163db6ccf00dbe68402ce55d6319b3edc33c421f31e9583b587b2de8a118f","version":1},"serverInfo":{"name":"muse","version":"1.4.1"},"sessionDurability":"durable","userAgent":"muse-build/1.4.1 (non-interactive; linux-x86_64; build 35815c253477406321e3f5595195becc25e3907a)"}}} +{"dir":"out","msg":{"jsonrpc":"2.0","method":"initialized","params":{}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":2,"method":"session/resume","params":{"commandId":"01a0ee48-763e-7f15-afc7-4727ca9099e9","sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","cursor":"v:01a0ee48-3713-70a0-bab7-ab463b7bc580:12"}}} +{"dir":"in","msg":{"emittedAtMs":1790704121503,"jsonrpc":"2.0","method":"session/branchChanged","params":{"branch":"main","sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","sourceRange":{"first":{"id":"64388c89-c4e0-409e-8a86-3cdecafea128","sequence":69},"last":{"id":"64388c89-c4e0-409e-8a86-3cdecafea128","sequence":69},"stream":{"id":"01a0ee48-3713-70a0-bab7-ab463b7bc580","kind":"session"}},"vcs":"git","viewCursor":"v:01a0ee48-3713-70a0-bab7-ab463b7bc580:13","workspaceRoot":"/tmp/muse-phase0/repo"}}} +{"dir":"in","msg":{"emittedAtMs":1790704121503,"jsonrpc":"2.0","method":"session/branchChanged","params":{"branch":"main","sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","sourceRange":{"first":{"id":"8d5ce3e0-18d8-4a82-9374-08d6d6ca7b8f","sequence":75},"last":{"id":"8d5ce3e0-18d8-4a82-9374-08d6d6ca7b8f","sequence":75},"stream":{"id":"01a0ee48-3713-70a0-bab7-ab463b7bc580","kind":"session"}},"vcs":"git","viewCursor":"v:01a0ee48-3713-70a0-bab7-ab463b7bc580:14","workspaceRoot":"/tmp/muse-phase0/repo"}}} +{"dir":"in","msg":{"emittedAtMs":1790704121503,"jsonrpc":"2.0","method":"turn/started","params":{"commandId":"01a0ee48-4e9f-7119-afdc-f5793f43307d","sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","sourceRange":{"first":{"id":"263cb24d-ea39-436e-a27d-933e5c8c63b9","sequence":77},"last":{"id":"263cb24d-ea39-436e-a27d-933e5c8c63b9","sequence":77},"stream":{"id":"01a0ee48-3713-70a0-bab7-ab463b7bc580","kind":"session"}},"turnId":"01a0ee48-4e9f-7119-afdc-f5793f43307d","viewCursor":"v:01a0ee48-3713-70a0-bab7-ab463b7bc580:15"}}} +{"dir":"in","msg":{"emittedAtMs":1790704121503,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"commandId":"01a0ee48-4e9f-7119-afdc-f5793f43307d","itemId":"a54df7ca-e35f-42f5-884f-2ced92241bc2","kind":"userMessage","recordedAt":"2026-09-29T17:48:31.26526Z","revision":1,"status":"completed","text":"What marker did you just reply? Reply only that marker, without tools.","turnId":"01a0ee48-4e9f-7119-afdc-f5793f43307d"},"sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","sourceRange":{"first":{"id":"263cb24d-ea39-436e-a27d-933e5c8c63b9","sequence":77},"last":{"id":"263cb24d-ea39-436e-a27d-933e5c8c63b9","sequence":77},"stream":{"id":"01a0ee48-3713-70a0-bab7-ab463b7bc580","kind":"session"}},"viewCursor":"v:01a0ee48-3713-70a0-bab7-ab463b7bc580:16"}}} +{"dir":"in","msg":{"emittedAtMs":1790704121503,"jsonrpc":"2.0","method":"item/started","params":{"item":{"childSessionId":"ba7d5afb-1ee2-445f-a7bc-acdaa35e2149","fallbackText":"Reminder child session","generationId":1,"itemId":"a16c82ca-103a-48ee-97e5-04bf9f49da6b","kind":"reminderChild","recordedAt":"2026-09-29T17:48:31.269224Z","reminderAgentId":"skill-reminder","revision":1,"status":"inProgress","taskId":"01a0ee48-4ea4-7fb0-8d4e-b9f2584e3354","turnId":"01a0ee48-4e9f-7119-afdc-f5793f43307d"},"sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","sourceRange":{"first":{"id":"a16c82ca-103a-48ee-97e5-04bf9f49da6b","sequence":92},"last":{"id":"a16c82ca-103a-48ee-97e5-04bf9f49da6b","sequence":92},"stream":{"id":"01a0ee48-3713-70a0-bab7-ab463b7bc580","kind":"session"}},"viewCursor":"v:01a0ee48-3713-70a0-bab7-ab463b7bc580:17"}}} +{"dir":"in","msg":{"emittedAtMs":1790704121503,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"itemId":"d8ae849a-116e-4574-8f0b-54d5679e2f49","kind":"agentMessage","recordedAt":"2026-09-29T17:48:34.030457Z","revision":2,"status":"completed","text":"RESUME_MARKER_OK","turnId":"01a0ee48-4e9f-7119-afdc-f5793f43307d"},"sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","sourceRange":{"first":{"id":"9e2ea853-13bd-487e-ae14-9d525846a585","sequence":105},"last":{"id":"9e2ea853-13bd-487e-ae14-9d525846a585","sequence":105},"stream":{"id":"01a0ee48-3713-70a0-bab7-ab463b7bc580","kind":"session"}},"viewCursor":"v:01a0ee48-3713-70a0-bab7-ab463b7bc580:18"}}} +{"dir":"in","msg":{"emittedAtMs":1790704121503,"jsonrpc":"2.0","method":"session/tokenUsage","params":{"cumulative":{"outputTokens":80,"promptTokens":73806,"totalTokens":73886},"durationMs":2763,"modelId":"meta/muse-spark-1.3","promptTokens":35676,"sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","sourceRange":{"first":{"id":"083d7c06-b769-4c3d-8dbe-4b024476e0ef","sequence":108},"last":{"id":"083d7c06-b769-4c3d-8dbe-4b024476e0ef","sequence":108},"stream":{"id":"01a0ee48-3713-70a0-bab7-ab463b7bc580","kind":"session"}},"totalTokens":35729,"turnId":"01a0ee48-4e9f-7119-afdc-f5793f43307d","usage":{"cacheReadTokens":18033,"cacheWriteTokens":0,"cachedTokens":18033,"inputTokens":35676,"outputTokens":53,"reasoningTokens":38},"viewCursor":"v:01a0ee48-3713-70a0-bab7-ab463b7bc580:19"}}} +{"dir":"in","msg":{"emittedAtMs":1790704121503,"jsonrpc":"2.0","method":"item/started","params":{"item":{"childSessionId":"3944baa3-651a-4816-9825-fcb67f059596","fallbackText":"Reminder child session","generationId":1,"itemId":"2651312c-6d68-406c-8fc7-18b2dc964083","kind":"reminderChild","recordedAt":"2026-09-29T17:48:34.034676Z","reminderAgentId":"verify-reminder","revision":1,"status":"inProgress","taskId":"01a0ee48-5972-77e2-8d85-c6afaf347ed8","turnId":"01a0ee48-4e9f-7119-afdc-f5793f43307d"},"sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","sourceRange":{"first":{"id":"2651312c-6d68-406c-8fc7-18b2dc964083","sequence":115},"last":{"id":"2651312c-6d68-406c-8fc7-18b2dc964083","sequence":115},"stream":{"id":"01a0ee48-3713-70a0-bab7-ab463b7bc580","kind":"session"}},"viewCursor":"v:01a0ee48-3713-70a0-bab7-ab463b7bc580:20"}}} +{"dir":"in","msg":{"emittedAtMs":1790704121503,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"childSessionId":"ba7d5afb-1ee2-445f-a7bc-acdaa35e2149","fallbackText":"Reminder child session","generationId":1,"itemId":"a16c82ca-103a-48ee-97e5-04bf9f49da6b","kind":"reminderChild","recordedAt":"2026-09-29T17:48:41.074443Z","reminderAgentId":"skill-reminder","revision":2,"status":"completed","taskId":"01a0ee48-4ea4-7fb0-8d4e-b9f2584e3354","turnId":"01a0ee48-4e9f-7119-afdc-f5793f43307d"},"sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","sourceRange":{"first":{"id":"a16c82ca-103a-48ee-97e5-04bf9f49da6b","sequence":92},"last":{"id":"cf508ab3-de8d-44ba-a0e6-84b54d031627","sequence":117},"stream":{"id":"01a0ee48-3713-70a0-bab7-ab463b7bc580","kind":"session"}},"viewCursor":"v:01a0ee48-3713-70a0-bab7-ab463b7bc580:21"}}} +{"dir":"in","msg":{"emittedAtMs":1790704121503,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"childSessionId":"3944baa3-651a-4816-9825-fcb67f059596","fallbackText":"Reminder child session","generationId":1,"itemId":"2651312c-6d68-406c-8fc7-18b2dc964083","kind":"reminderChild","recordedAt":"2026-09-29T17:48:41.074657Z","reminderAgentId":"verify-reminder","revision":2,"status":"completed","taskId":"01a0ee48-5972-77e2-8d85-c6afaf347ed8","turnId":"01a0ee48-4e9f-7119-afdc-f5793f43307d"},"sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","sourceRange":{"first":{"id":"2651312c-6d68-406c-8fc7-18b2dc964083","sequence":115},"last":{"id":"bf9401ff-ae66-47cb-a7e4-0305000df728","sequence":118},"stream":{"id":"01a0ee48-3713-70a0-bab7-ab463b7bc580","kind":"session"}},"viewCursor":"v:01a0ee48-3713-70a0-bab7-ab463b7bc580:22"}}} +{"dir":"in","msg":{"emittedAtMs":1790704121503,"jsonrpc":"2.0","method":"turn/completed","params":{"durationMs":9809,"sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","sourceRange":{"first":{"id":"08714520-df9a-42d2-8919-baf78f39b8aa","sequence":124},"last":{"id":"08714520-df9a-42d2-8919-baf78f39b8aa","sequence":124},"stream":{"id":"01a0ee48-3713-70a0-bab7-ab463b7bc580","kind":"session"}},"terminal":"completed","timeToFirstTokenMs":2697,"turnId":"01a0ee48-4e9f-7119-afdc-f5793f43307d","viewCursor":"v:01a0ee48-3713-70a0-bab7-ab463b7bc580:23"}}} +{"dir":"in","msg":{"emittedAtMs":1790704121503,"jsonrpc":"2.0","method":"session/branchChanged","params":{"branch":"main","sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","sourceRange":{"first":{"id":"dd04e5a6-68ca-457b-9ef1-c169cfbb84f5","sequence":125},"last":{"id":"dd04e5a6-68ca-457b-9ef1-c169cfbb84f5","sequence":125},"stream":{"id":"01a0ee48-3713-70a0-bab7-ab463b7bc580","kind":"session"}},"vcs":"git","viewCursor":"v:01a0ee48-3713-70a0-bab7-ab463b7bc580:24","workspaceRoot":"/tmp/muse-phase0/repo"}}} +{"dir":"in","msg":{"emittedAtMs":1790704121503,"jsonrpc":"2.0","method":"session/branchChanged","params":{"branch":"main","sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","sourceRange":{"first":{"id":"cfa5b0f8-0cd4-4849-9496-58a47d39e026","sequence":128},"last":{"id":"cfa5b0f8-0cd4-4849-9496-58a47d39e026","sequence":128},"stream":{"id":"01a0ee48-3713-70a0-bab7-ab463b7bc580","kind":"session"}},"vcs":"git","viewCursor":"v:01a0ee48-3713-70a0-bab7-ab463b7bc580:25","workspaceRoot":"/tmp/muse-phase0/repo"}}} +{"dir":"in","msg":{"id":2,"jsonrpc":"2.0","result":{"history":{"items":null,"mode":"none","noneReason":"cursorSuffix","snapshot":null},"pendingRequests":[],"session":{"activeTurnId":null,"approvalMode":{"lastCommandId":null,"mode":"onRequest","source":"replay"},"branch":"main","createdAt":"2026-09-29T17:48:25.236035Z","firstUserPrompt":"Reply exactly RESUME_MARKER_OK without tools.","forkedFrom":null,"lastActivityAt":"2026-09-29T17:48:41.075126Z","modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee48-3713-70a0-bab7-ab463b7bc580/session.jsonl","providerId":"meta","sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","status":"idle","title":"Reply exactly RESUME_MARKER_OK without tools.","turnCount":2,"updatedAt":"2026-09-29T17:48:41.422766Z","workspaceRoot":"/tmp/muse-phase0/repo"},"viewCursor":"v:01a0ee48-3713-70a0-bab7-ab463b7bc580:25"}}} +{"dir":"in","msg":{"emittedAtMs":1790704121509,"jsonrpc":"2.0","method":"session/branchChanged","params":{"branch":"main","sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","sourceRange":{"first":{"id":"cfa5b0f8-0cd4-4849-9496-58a47d39e026","sequence":128},"last":{"id":"cfa5b0f8-0cd4-4849-9496-58a47d39e026","sequence":128},"stream":{"id":"01a0ee48-3713-70a0-bab7-ab463b7bc580","kind":"session"}},"vcs":"git","viewCursor":"v:01a0ee48-3713-70a0-bab7-ab463b7bc580:25","workspaceRoot":"/tmp/muse-phase0/repo"}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":3,"method":"turn/start","params":{"commandId":"01a0ee48-76a5-7393-a8c3-e14e089b52f0","sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","input":[{"type":"text","text":"What marker did you just reply? Reply only that marker, without tools."}],"reasoningEffort":"low"}}} +{"dir":"in","msg":{"emittedAtMs":1790704121510,"jsonrpc":"2.0","method":"session/branchChanged","params":{"branch":"main","sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","sourceRange":{"first":{"id":"2ee1047a-6a1f-4b43-898b-6e5834143d11","sequence":134},"last":{"id":"2ee1047a-6a1f-4b43-898b-6e5834143d11","sequence":134},"stream":{"id":"01a0ee48-3713-70a0-bab7-ab463b7bc580","kind":"session"}},"vcs":"git","viewCursor":"v:01a0ee48-3713-70a0-bab7-ab463b7bc580:26","workspaceRoot":"/tmp/muse-phase0/repo"}}} +{"dir":"in","msg":{"id":3,"jsonrpc":"2.0","result":{"commandId":"01a0ee48-76a5-7393-a8c3-e14e089b52f0","disposition":"started","startedNewTurn":true,"status":"accepted","turnId":"01a0ee48-76a5-7393-a8c3-e14e089b52f0"}}} +{"dir":"in","msg":{"emittedAtMs":1790704121518,"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","status":"running","viewCursor":"v:01a0ee48-3713-70a0-bab7-ab463b7bc580:27"}}} +{"dir":"in","msg":{"emittedAtMs":1790704121519,"jsonrpc":"2.0","method":"turn/started","params":{"commandId":"01a0ee48-76a5-7393-a8c3-e14e089b52f0","sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","sourceRange":{"first":{"id":"0dc78b21-3a29-43ff-ad87-74d725e5253b","sequence":136},"last":{"id":"0dc78b21-3a29-43ff-ad87-74d725e5253b","sequence":136},"stream":{"id":"01a0ee48-3713-70a0-bab7-ab463b7bc580","kind":"session"}},"turnId":"01a0ee48-76a5-7393-a8c3-e14e089b52f0","viewCursor":"v:01a0ee48-3713-70a0-bab7-ab463b7bc580:27"}}} +{"dir":"in","msg":{"emittedAtMs":1790704121519,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"commandId":"01a0ee48-76a5-7393-a8c3-e14e089b52f0","itemId":"64f515a8-60db-47a1-b9f8-84cc1e226145","kind":"userMessage","recordedAt":"2026-09-29T17:48:41.511669Z","revision":1,"status":"completed","text":"What marker did you just reply? Reply only that marker, without tools.","turnId":"01a0ee48-76a5-7393-a8c3-e14e089b52f0"},"sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","sourceRange":{"first":{"id":"0dc78b21-3a29-43ff-ad87-74d725e5253b","sequence":136},"last":{"id":"0dc78b21-3a29-43ff-ad87-74d725e5253b","sequence":136},"stream":{"id":"01a0ee48-3713-70a0-bab7-ab463b7bc580","kind":"session"}},"viewCursor":"v:01a0ee48-3713-70a0-bab7-ab463b7bc580:28"}}} +{"dir":"in","msg":{"emittedAtMs":1790704121519,"jsonrpc":"2.0","method":"item/started","params":{"item":{"childSessionId":"461905f8-cc17-4cbe-a1c1-050cf0836e6b","fallbackText":"Reminder child session","generationId":1,"itemId":"29bb3839-3044-488c-8553-9f9e4b238249","kind":"reminderChild","recordedAt":"2026-09-29T17:48:41.514846Z","reminderAgentId":"skill-reminder","revision":1,"status":"inProgress","taskId":"01a0ee48-76aa-76b0-b00a-1e778326da40","turnId":"01a0ee48-76a5-7393-a8c3-e14e089b52f0"},"sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","sourceRange":{"first":{"id":"29bb3839-3044-488c-8553-9f9e4b238249","sequence":151},"last":{"id":"29bb3839-3044-488c-8553-9f9e4b238249","sequence":151},"stream":{"id":"01a0ee48-3713-70a0-bab7-ab463b7bc580","kind":"session"}},"viewCursor":"v:01a0ee48-3713-70a0-bab7-ab463b7bc580:29"}}} +{"dir":"in","msg":{"emittedAtMs":1790704121519,"jsonrpc":"2.0","method":"session/listChanged","params":{"session":{"activeTurnId":"01a0ee48-76a5-7393-a8c3-e14e089b52f0","branch":"main","createdAt":"2026-09-29T17:48:25.236035Z","firstUserPrompt":"Reply exactly RESUME_MARKER_OK without tools.","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee48-3713-70a0-bab7-ab463b7bc580/session.jsonl","providerId":"meta","sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","status":"running","title":"Reply exactly RESUME_MARKER_OK without tools.","turnCount":2,"updatedAt":"2026-09-29T17:48:41.516193Z","workspaceRoot":"/tmp/muse-phase0/repo"}}}} +{"dir":"in","msg":{"emittedAtMs":1790704125163,"jsonrpc":"2.0","method":"item/started","params":{"item":{"itemId":"0db738b7-658c-476c-8835-c84c6ddecd54","kind":"agentMessage","revision":1,"status":"inProgress","text":"","turnId":"01a0ee48-76a5-7393-a8c3-e14e089b52f0"},"sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","viewCursor":"v:01a0ee48-3713-70a0-bab7-ab463b7bc580:29.1"}}} +{"dir":"in","msg":{"emittedAtMs":1790704125163,"jsonrpc":"2.0","method":"item/delta","params":{"delta":"RESUME_MARKER_OK","field":"text","itemId":"0db738b7-658c-476c-8835-c84c6ddecd54","sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","viewCursor":"v:01a0ee48-3713-70a0-bab7-ab463b7bc580:29.2"}}} +{"dir":"in","msg":{"emittedAtMs":1790704125256,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"itemId":"0db738b7-658c-476c-8835-c84c6ddecd54","kind":"agentMessage","recordedAt":"2026-09-29T17:48:45.256357Z","revision":2,"status":"completed","text":"RESUME_MARKER_OK","turnId":"01a0ee48-76a5-7393-a8c3-e14e089b52f0"},"sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","sourceRange":{"first":{"id":"d973348a-bea5-4a57-8214-ec2bba989f5d","sequence":164},"last":{"id":"d973348a-bea5-4a57-8214-ec2bba989f5d","sequence":164},"stream":{"id":"01a0ee48-3713-70a0-bab7-ab463b7bc580","kind":"session"}},"viewCursor":"v:01a0ee48-3713-70a0-bab7-ab463b7bc580:30"}}} +{"dir":"in","msg":{"emittedAtMs":1790704125260,"jsonrpc":"2.0","method":"session/tokenUsage","params":{"cumulative":{"outputTokens":123,"promptTokens":109558,"totalTokens":109681},"durationMs":3744,"modelId":"meta/muse-spark-1.3","promptTokens":35752,"sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","sourceRange":{"first":{"id":"2fcc5b33-1c74-455c-a4cf-c45c83514ce2","sequence":167},"last":{"id":"2fcc5b33-1c74-455c-a4cf-c45c83514ce2","sequence":167},"stream":{"id":"01a0ee48-3713-70a0-bab7-ab463b7bc580","kind":"session"}},"totalTokens":35795,"turnId":"01a0ee48-76a5-7393-a8c3-e14e089b52f0","usage":{"cacheReadTokens":35185,"cacheWriteTokens":0,"cachedTokens":35185,"inputTokens":35752,"outputTokens":43,"reasoningTokens":28},"viewCursor":"v:01a0ee48-3713-70a0-bab7-ab463b7bc580:31"}}} +{"dir":"in","msg":{"emittedAtMs":1790704125260,"jsonrpc":"2.0","method":"item/started","params":{"item":{"childSessionId":"cd242dac-6012-404d-b95a-4de310c201f7","fallbackText":"Reminder child session","generationId":1,"itemId":"8a3e4e65-1300-41a0-8b12-e22be2b81b83","kind":"reminderChild","recordedAt":"2026-09-29T17:48:45.260754Z","reminderAgentId":"verify-reminder","revision":1,"status":"inProgress","taskId":"01a0ee48-854c-7311-8e8d-e215dad84b7a","turnId":"01a0ee48-76a5-7393-a8c3-e14e089b52f0"},"sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","sourceRange":{"first":{"id":"8a3e4e65-1300-41a0-8b12-e22be2b81b83","sequence":174},"last":{"id":"8a3e4e65-1300-41a0-8b12-e22be2b81b83","sequence":174},"stream":{"id":"01a0ee48-3713-70a0-bab7-ab463b7bc580","kind":"session"}},"viewCursor":"v:01a0ee48-3713-70a0-bab7-ab463b7bc580:32"}}} +{"dir":"in","msg":{"emittedAtMs":1790704128411,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"childSessionId":"cd242dac-6012-404d-b95a-4de310c201f7","fallbackText":"Reminder child session","generationId":1,"itemId":"8a3e4e65-1300-41a0-8b12-e22be2b81b83","kind":"reminderChild","recordedAt":"2026-09-29T17:48:48.411846Z","reminderAgentId":"verify-reminder","revision":2,"status":"completed","taskId":"01a0ee48-854c-7311-8e8d-e215dad84b7a","turnId":"01a0ee48-76a5-7393-a8c3-e14e089b52f0"},"sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","sourceRange":{"first":{"id":"8a3e4e65-1300-41a0-8b12-e22be2b81b83","sequence":174},"last":{"id":"f97a5fcb-0c3c-475f-99b0-e2f024fb5bc1","sequence":176},"stream":{"id":"01a0ee48-3713-70a0-bab7-ab463b7bc580","kind":"session"}},"viewCursor":"v:01a0ee48-3713-70a0-bab7-ab463b7bc580:33"}}} +{"dir":"in","msg":{"emittedAtMs":1790704128414,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"childSessionId":"461905f8-cc17-4cbe-a1c1-050cf0836e6b","failureReason":"main run completed","fallbackText":"Reminder child session","generationId":1,"itemId":"29bb3839-3044-488c-8553-9f9e4b238249","kind":"reminderChild","recordedAt":"2026-09-29T17:48:48.41459Z","reminderAgentId":"skill-reminder","revision":2,"status":"cancelled","taskId":"01a0ee48-76aa-76b0-b00a-1e778326da40","turnId":"01a0ee48-76a5-7393-a8c3-e14e089b52f0"},"sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","sourceRange":{"first":{"id":"29bb3839-3044-488c-8553-9f9e4b238249","sequence":151},"last":{"id":"d5d80db2-a75e-42f6-9e02-af7eee6c0b03","sequence":180},"stream":{"id":"01a0ee48-3713-70a0-bab7-ab463b7bc580","kind":"session"}},"viewCursor":"v:01a0ee48-3713-70a0-bab7-ab463b7bc580:34"}}} +{"dir":"in","msg":{"emittedAtMs":1790704128414,"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","status":"idle","viewCursor":"v:01a0ee48-3713-70a0-bab7-ab463b7bc580:35"}}} +{"dir":"in","msg":{"emittedAtMs":1790704128415,"jsonrpc":"2.0","method":"turn/completed","params":{"durationMs":6903,"sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","sourceRange":{"first":{"id":"c9ab8973-00a7-4cdc-9779-bde0590469b0","sequence":182},"last":{"id":"c9ab8973-00a7-4cdc-9779-bde0590469b0","sequence":182},"stream":{"id":"01a0ee48-3713-70a0-bab7-ab463b7bc580","kind":"session"}},"terminal":"completed","timeToFirstTokenMs":3651,"turnId":"01a0ee48-76a5-7393-a8c3-e14e089b52f0","viewCursor":"v:01a0ee48-3713-70a0-bab7-ab463b7bc580:35"}}} +{"dir":"in","msg":{"emittedAtMs":1790704128416,"jsonrpc":"2.0","method":"session/listChanged","params":{"session":{"activeTurnId":null,"branch":"main","createdAt":"2026-09-29T17:48:25.236035Z","firstUserPrompt":"Reply exactly RESUME_MARKER_OK without tools.","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee48-3713-70a0-bab7-ab463b7bc580/session.jsonl","providerId":"meta","sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","status":"idle","title":"Reply exactly RESUME_MARKER_OK without tools.","turnCount":3,"updatedAt":"2026-09-29T17:48:48.414748Z","workspaceRoot":"/tmp/muse-phase0/repo"}}}} +{"dir":"in","msg":{"emittedAtMs":1790704128629,"jsonrpc":"2.0","method":"session/closed","params":{"reason":"hostShutdown","sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","viewCursor":"v:01a0ee48-3713-70a0-bab7-ab463b7bc580:36"}}} +{"dir":"in","msg":{"emittedAtMs":1790704128629,"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","status":"notLoaded","viewCursor":"v:01a0ee48-3713-70a0-bab7-ab463b7bc580:36"}}} +{"dir":"in","msg":{"emittedAtMs":1790704128629,"jsonrpc":"2.0","method":"session/listChanged","params":{"session":{"activeTurnId":null,"branch":"main","createdAt":"2026-09-29T17:48:25.236035Z","firstUserPrompt":"Reply exactly RESUME_MARKER_OK without tools.","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee48-3713-70a0-bab7-ab463b7bc580/session.jsonl","providerId":"meta","sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","status":"notLoaded","title":"Reply exactly RESUME_MARKER_OK without tools.","turnCount":3,"updatedAt":"2026-09-29T17:48:48.41637Z","workspaceRoot":"/tmp/muse-phase0/repo"}}}} diff --git a/plugins/muse-provider/test/fixtures/resume-without-cursor.ndjson b/plugins/muse-provider/test/fixtures/resume-without-cursor.ndjson new file mode 100644 index 0000000000..3a1c0184db --- /dev/null +++ b/plugins/muse-provider/test/fixtures/resume-without-cursor.ndjson @@ -0,0 +1,28 @@ +{"dir":"out","msg":{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"clientInfo":{"name":"paseo_phase0","version":"0.0.0"},"capabilities":{"experimentalApi":true,"requestedCapabilities":["sessionMcp","sessionListStream"]}}}} +{"dir":"in","msg":{"id":1,"jsonrpc":"2.0","result":{"experimentalApi":true,"grantedCapabilities":["sessionMcp","sessionListStream"],"museHome":"/tmp/muse-phase0/data/muse","platformFamily":"unix","platformOs":"linux","schema":{"fingerprint":"sha256:e0e163db6ccf00dbe68402ce55d6319b3edc33c421f31e9583b587b2de8a118f","version":1},"serverInfo":{"name":"muse","version":"1.4.1"},"sessionDurability":"durable","userAgent":"muse-build/1.4.1 (non-interactive; linux-x86_64; build 35815c253477406321e3f5595195becc25e3907a)"}}} +{"dir":"out","msg":{"jsonrpc":"2.0","method":"initialized","params":{}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":2,"method":"session/resume","params":{"commandId":"01a0ee48-4e3d-7dab-ac35-df9392bc6f4d","sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580"}}} +{"dir":"in","msg":{"emittedAtMs":1790704111255,"jsonrpc":"2.0","method":"session/branchChanged","params":{"branch":"main","sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","sourceRange":{"first":{"id":"64388c89-c4e0-409e-8a86-3cdecafea128","sequence":69},"last":{"id":"64388c89-c4e0-409e-8a86-3cdecafea128","sequence":69},"stream":{"id":"01a0ee48-3713-70a0-bab7-ab463b7bc580","kind":"session"}},"vcs":"git","viewCursor":"v:01a0ee48-3713-70a0-bab7-ab463b7bc580:13","workspaceRoot":"/tmp/muse-phase0/repo"}}} +{"dir":"in","msg":{"id":2,"jsonrpc":"2.0","result":{"history":{"items":[{"commandId":"01a0ee48-376d-7cff-a66c-5e1ac7035295","itemId":"21b34e05-d5c5-492b-b166-b261039b69b1","kind":"userMessage","recordedAt":"2026-09-29T17:48:25.328624Z","revision":1,"status":"completed","text":"Reply exactly RESUME_MARKER_OK without tools.","turnId":"01a0ee48-376d-7cff-a66c-5e1ac7035295"},{"childSessionId":"76293abd-9c8a-4a09-8edf-377e9e17bf81","failureReason":"main run completed","fallbackText":"Reminder child session","generationId":1,"itemId":"1275560b-0426-4de0-91c2-e76c1698e73b","kind":"reminderChild","recordedAt":"2026-09-29T17:48:30.818129Z","reminderAgentId":"skill-reminder","revision":2,"status":"cancelled","taskId":"01a0ee48-3776-7a82-b1bc-b843d01dbbf3","turnId":"01a0ee48-376d-7cff-a66c-5e1ac7035295"},{"itemId":"141a95ec-0b8f-47fe-9e95-c3cfcc50b8a5","kind":"agentMessage","recordedAt":"2026-09-29T17:48:28.508252Z","revision":2,"status":"completed","text":"RESUME_MARKER_OK","turnId":"01a0ee48-376d-7cff-a66c-5e1ac7035295"},{"childSessionId":"e9f3e28f-f760-4a06-8127-543e89e5ca72","childSessionLogPath":"subagent/e9f3e28f-f760-4a06-8127-543e89e5ca72/session.jsonl","fallbackText":"Reminder child session","generationId":1,"itemId":"bebe2029-2f83-4cbb-9ac5-6da291835a0c","kind":"reminderChild","recordedAt":"2026-09-29T17:48:30.814728Z","reminderAgentId":"verify-reminder","revision":2,"status":"completed","taskId":"01a0ee48-43e7-7921-8a10-bbfc1f118cdc","turnId":"01a0ee48-376d-7cff-a66c-5e1ac7035295"}],"mode":"inline","snapshot":null},"pendingRequests":[],"session":{"activeTurnId":null,"approvalMode":{"lastCommandId":null,"mode":"onRequest","source":"replay"},"branch":"main","createdAt":"2026-09-29T17:48:25.236035Z","firstUserPrompt":"Reply exactly RESUME_MARKER_OK without tools.","forkedFrom":null,"lastActivityAt":"2026-09-29T17:48:30.818304Z","modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee48-3713-70a0-bab7-ab463b7bc580/session.jsonl","providerId":"meta","sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","status":"idle","title":"Reply exactly RESUME_MARKER_OK without tools.","turnCount":1,"updatedAt":"2026-09-29T17:48:31.178029Z","workspaceRoot":"/tmp/muse-phase0/repo"},"viewCursor":"v:01a0ee48-3713-70a0-bab7-ab463b7bc580:13"}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":3,"method":"turn/start","params":{"commandId":"01a0ee48-4e9f-7119-afdc-f5793f43307d","sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","input":[{"type":"text","text":"What marker did you just reply? Reply only that marker, without tools."}],"reasoningEffort":"low"}}} +{"dir":"in","msg":{"emittedAtMs":1790704111264,"jsonrpc":"2.0","method":"session/branchChanged","params":{"branch":"main","sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","sourceRange":{"first":{"id":"8d5ce3e0-18d8-4a82-9374-08d6d6ca7b8f","sequence":75},"last":{"id":"8d5ce3e0-18d8-4a82-9374-08d6d6ca7b8f","sequence":75},"stream":{"id":"01a0ee48-3713-70a0-bab7-ab463b7bc580","kind":"session"}},"vcs":"git","viewCursor":"v:01a0ee48-3713-70a0-bab7-ab463b7bc580:14","workspaceRoot":"/tmp/muse-phase0/repo"}}} +{"dir":"in","msg":{"id":3,"jsonrpc":"2.0","result":{"commandId":"01a0ee48-4e9f-7119-afdc-f5793f43307d","disposition":"started","startedNewTurn":true,"status":"accepted","turnId":"01a0ee48-4e9f-7119-afdc-f5793f43307d"}}} +{"dir":"in","msg":{"emittedAtMs":1790704111274,"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","status":"running","viewCursor":"v:01a0ee48-3713-70a0-bab7-ab463b7bc580:15"}}} +{"dir":"in","msg":{"emittedAtMs":1790704111275,"jsonrpc":"2.0","method":"turn/started","params":{"commandId":"01a0ee48-4e9f-7119-afdc-f5793f43307d","sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","sourceRange":{"first":{"id":"263cb24d-ea39-436e-a27d-933e5c8c63b9","sequence":77},"last":{"id":"263cb24d-ea39-436e-a27d-933e5c8c63b9","sequence":77},"stream":{"id":"01a0ee48-3713-70a0-bab7-ab463b7bc580","kind":"session"}},"turnId":"01a0ee48-4e9f-7119-afdc-f5793f43307d","viewCursor":"v:01a0ee48-3713-70a0-bab7-ab463b7bc580:15"}}} +{"dir":"in","msg":{"emittedAtMs":1790704111275,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"commandId":"01a0ee48-4e9f-7119-afdc-f5793f43307d","itemId":"a54df7ca-e35f-42f5-884f-2ced92241bc2","kind":"userMessage","recordedAt":"2026-09-29T17:48:31.26526Z","revision":1,"status":"completed","text":"What marker did you just reply? Reply only that marker, without tools.","turnId":"01a0ee48-4e9f-7119-afdc-f5793f43307d"},"sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","sourceRange":{"first":{"id":"263cb24d-ea39-436e-a27d-933e5c8c63b9","sequence":77},"last":{"id":"263cb24d-ea39-436e-a27d-933e5c8c63b9","sequence":77},"stream":{"id":"01a0ee48-3713-70a0-bab7-ab463b7bc580","kind":"session"}},"viewCursor":"v:01a0ee48-3713-70a0-bab7-ab463b7bc580:16"}}} +{"dir":"in","msg":{"emittedAtMs":1790704111275,"jsonrpc":"2.0","method":"item/started","params":{"item":{"childSessionId":"ba7d5afb-1ee2-445f-a7bc-acdaa35e2149","fallbackText":"Reminder child session","generationId":1,"itemId":"a16c82ca-103a-48ee-97e5-04bf9f49da6b","kind":"reminderChild","recordedAt":"2026-09-29T17:48:31.269224Z","reminderAgentId":"skill-reminder","revision":1,"status":"inProgress","taskId":"01a0ee48-4ea4-7fb0-8d4e-b9f2584e3354","turnId":"01a0ee48-4e9f-7119-afdc-f5793f43307d"},"sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","sourceRange":{"first":{"id":"a16c82ca-103a-48ee-97e5-04bf9f49da6b","sequence":92},"last":{"id":"a16c82ca-103a-48ee-97e5-04bf9f49da6b","sequence":92},"stream":{"id":"01a0ee48-3713-70a0-bab7-ab463b7bc580","kind":"session"}},"viewCursor":"v:01a0ee48-3713-70a0-bab7-ab463b7bc580:17"}}} +{"dir":"in","msg":{"emittedAtMs":1790704111275,"jsonrpc":"2.0","method":"session/listChanged","params":{"session":{"activeTurnId":"01a0ee48-4e9f-7119-afdc-f5793f43307d","branch":"main","createdAt":"2026-09-29T17:48:25.236035Z","firstUserPrompt":"Reply exactly RESUME_MARKER_OK without tools.","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee48-3713-70a0-bab7-ab463b7bc580/session.jsonl","providerId":"meta","sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","status":"running","title":"Reply exactly RESUME_MARKER_OK without tools.","turnCount":1,"updatedAt":"2026-09-29T17:48:31.270933Z","workspaceRoot":"/tmp/muse-phase0/repo"}}}} +{"dir":"in","msg":{"emittedAtMs":1790704113963,"jsonrpc":"2.0","method":"item/started","params":{"item":{"itemId":"d8ae849a-116e-4574-8f0b-54d5679e2f49","kind":"agentMessage","revision":1,"status":"inProgress","text":"","turnId":"01a0ee48-4e9f-7119-afdc-f5793f43307d"},"sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","viewCursor":"v:01a0ee48-3713-70a0-bab7-ab463b7bc580:17.1"}}} +{"dir":"in","msg":{"emittedAtMs":1790704113963,"jsonrpc":"2.0","method":"item/delta","params":{"delta":"RESUME_MARKER_OK","field":"text","itemId":"d8ae849a-116e-4574-8f0b-54d5679e2f49","sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","viewCursor":"v:01a0ee48-3713-70a0-bab7-ab463b7bc580:17.2"}}} +{"dir":"in","msg":{"emittedAtMs":1790704114030,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"itemId":"d8ae849a-116e-4574-8f0b-54d5679e2f49","kind":"agentMessage","recordedAt":"2026-09-29T17:48:34.030457Z","revision":2,"status":"completed","text":"RESUME_MARKER_OK","turnId":"01a0ee48-4e9f-7119-afdc-f5793f43307d"},"sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","sourceRange":{"first":{"id":"9e2ea853-13bd-487e-ae14-9d525846a585","sequence":105},"last":{"id":"9e2ea853-13bd-487e-ae14-9d525846a585","sequence":105},"stream":{"id":"01a0ee48-3713-70a0-bab7-ab463b7bc580","kind":"session"}},"viewCursor":"v:01a0ee48-3713-70a0-bab7-ab463b7bc580:18"}}} +{"dir":"in","msg":{"emittedAtMs":1790704114034,"jsonrpc":"2.0","method":"session/tokenUsage","params":{"cumulative":{"outputTokens":80,"promptTokens":73806,"totalTokens":73886},"durationMs":2763,"modelId":"meta/muse-spark-1.3","promptTokens":35676,"sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","sourceRange":{"first":{"id":"083d7c06-b769-4c3d-8dbe-4b024476e0ef","sequence":108},"last":{"id":"083d7c06-b769-4c3d-8dbe-4b024476e0ef","sequence":108},"stream":{"id":"01a0ee48-3713-70a0-bab7-ab463b7bc580","kind":"session"}},"totalTokens":35729,"turnId":"01a0ee48-4e9f-7119-afdc-f5793f43307d","usage":{"cacheReadTokens":18033,"cacheWriteTokens":0,"cachedTokens":18033,"inputTokens":35676,"outputTokens":53,"reasoningTokens":38},"viewCursor":"v:01a0ee48-3713-70a0-bab7-ab463b7bc580:19"}}} +{"dir":"in","msg":{"emittedAtMs":1790704114034,"jsonrpc":"2.0","method":"item/started","params":{"item":{"childSessionId":"3944baa3-651a-4816-9825-fcb67f059596","fallbackText":"Reminder child session","generationId":1,"itemId":"2651312c-6d68-406c-8fc7-18b2dc964083","kind":"reminderChild","recordedAt":"2026-09-29T17:48:34.034676Z","reminderAgentId":"verify-reminder","revision":1,"status":"inProgress","taskId":"01a0ee48-5972-77e2-8d85-c6afaf347ed8","turnId":"01a0ee48-4e9f-7119-afdc-f5793f43307d"},"sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","sourceRange":{"first":{"id":"2651312c-6d68-406c-8fc7-18b2dc964083","sequence":115},"last":{"id":"2651312c-6d68-406c-8fc7-18b2dc964083","sequence":115},"stream":{"id":"01a0ee48-3713-70a0-bab7-ab463b7bc580","kind":"session"}},"viewCursor":"v:01a0ee48-3713-70a0-bab7-ab463b7bc580:20"}}} +{"dir":"in","msg":{"emittedAtMs":1790704121074,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"childSessionId":"ba7d5afb-1ee2-445f-a7bc-acdaa35e2149","fallbackText":"Reminder child session","generationId":1,"itemId":"a16c82ca-103a-48ee-97e5-04bf9f49da6b","kind":"reminderChild","recordedAt":"2026-09-29T17:48:41.074443Z","reminderAgentId":"skill-reminder","revision":2,"status":"completed","taskId":"01a0ee48-4ea4-7fb0-8d4e-b9f2584e3354","turnId":"01a0ee48-4e9f-7119-afdc-f5793f43307d"},"sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","sourceRange":{"first":{"id":"a16c82ca-103a-48ee-97e5-04bf9f49da6b","sequence":92},"last":{"id":"cf508ab3-de8d-44ba-a0e6-84b54d031627","sequence":117},"stream":{"id":"01a0ee48-3713-70a0-bab7-ab463b7bc580","kind":"session"}},"viewCursor":"v:01a0ee48-3713-70a0-bab7-ab463b7bc580:21"}}} +{"dir":"in","msg":{"emittedAtMs":1790704121074,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"childSessionId":"3944baa3-651a-4816-9825-fcb67f059596","fallbackText":"Reminder child session","generationId":1,"itemId":"2651312c-6d68-406c-8fc7-18b2dc964083","kind":"reminderChild","recordedAt":"2026-09-29T17:48:41.074657Z","reminderAgentId":"verify-reminder","revision":2,"status":"completed","taskId":"01a0ee48-5972-77e2-8d85-c6afaf347ed8","turnId":"01a0ee48-4e9f-7119-afdc-f5793f43307d"},"sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","sourceRange":{"first":{"id":"2651312c-6d68-406c-8fc7-18b2dc964083","sequence":115},"last":{"id":"bf9401ff-ae66-47cb-a7e4-0305000df728","sequence":118},"stream":{"id":"01a0ee48-3713-70a0-bab7-ab463b7bc580","kind":"session"}},"viewCursor":"v:01a0ee48-3713-70a0-bab7-ab463b7bc580:22"}}} +{"dir":"in","msg":{"emittedAtMs":1790704121075,"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","status":"idle","viewCursor":"v:01a0ee48-3713-70a0-bab7-ab463b7bc580:23"}}} +{"dir":"in","msg":{"emittedAtMs":1790704121076,"jsonrpc":"2.0","method":"turn/completed","params":{"durationMs":9809,"sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","sourceRange":{"first":{"id":"08714520-df9a-42d2-8919-baf78f39b8aa","sequence":124},"last":{"id":"08714520-df9a-42d2-8919-baf78f39b8aa","sequence":124},"stream":{"id":"01a0ee48-3713-70a0-bab7-ab463b7bc580","kind":"session"}},"terminal":"completed","timeToFirstTokenMs":2697,"turnId":"01a0ee48-4e9f-7119-afdc-f5793f43307d","viewCursor":"v:01a0ee48-3713-70a0-bab7-ab463b7bc580:23"}}} +{"dir":"in","msg":{"emittedAtMs":1790704121076,"jsonrpc":"2.0","method":"session/branchChanged","params":{"branch":"main","sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","sourceRange":{"first":{"id":"dd04e5a6-68ca-457b-9ef1-c169cfbb84f5","sequence":125},"last":{"id":"dd04e5a6-68ca-457b-9ef1-c169cfbb84f5","sequence":125},"stream":{"id":"01a0ee48-3713-70a0-bab7-ab463b7bc580","kind":"session"}},"vcs":"git","viewCursor":"v:01a0ee48-3713-70a0-bab7-ab463b7bc580:24","workspaceRoot":"/tmp/muse-phase0/repo"}}} +{"dir":"in","msg":{"emittedAtMs":1790704121076,"jsonrpc":"2.0","method":"session/listChanged","params":{"session":{"activeTurnId":null,"branch":"main","createdAt":"2026-09-29T17:48:25.236035Z","firstUserPrompt":"Reply exactly RESUME_MARKER_OK without tools.","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee48-3713-70a0-bab7-ab463b7bc580/session.jsonl","providerId":"meta","sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","status":"idle","title":"Reply exactly RESUME_MARKER_OK without tools.","turnCount":2,"updatedAt":"2026-09-29T17:48:41.076013Z","workspaceRoot":"/tmp/muse-phase0/repo"}}}} +{"dir":"in","msg":{"emittedAtMs":1790704121279,"jsonrpc":"2.0","method":"session/closed","params":{"reason":"hostShutdown","sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","viewCursor":"v:01a0ee48-3713-70a0-bab7-ab463b7bc580:24"}}} +{"dir":"in","msg":{"emittedAtMs":1790704121279,"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","status":"notLoaded","viewCursor":"v:01a0ee48-3713-70a0-bab7-ab463b7bc580:24"}}} +{"dir":"in","msg":{"emittedAtMs":1790704121279,"jsonrpc":"2.0","method":"session/listChanged","params":{"session":{"activeTurnId":null,"branch":"main","createdAt":"2026-09-29T17:48:25.236035Z","firstUserPrompt":"Reply exactly RESUME_MARKER_OK without tools.","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee48-3713-70a0-bab7-ab463b7bc580/session.jsonl","providerId":"meta","sessionId":"01a0ee48-3713-70a0-bab7-ab463b7bc580","status":"notLoaded","title":"Reply exactly RESUME_MARKER_OK without tools.","turnCount":2,"updatedAt":"2026-09-29T17:48:41.076013Z","workspaceRoot":"/tmp/muse-phase0/repo"}}}} diff --git a/plugins/muse-provider/test/fixtures/skill.ndjson b/plugins/muse-provider/test/fixtures/skill.ndjson new file mode 100644 index 0000000000..147579d231 --- /dev/null +++ b/plugins/muse-provider/test/fixtures/skill.ndjson @@ -0,0 +1,31 @@ +{"dir":"out","msg":{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"clientInfo":{"name":"paseo_phase0","version":"0.0.0"},"capabilities":{"experimentalApi":true,"requestedCapabilities":["sessionMcp","sessionListStream"]}}}} +{"dir":"in","msg":{"id":1,"jsonrpc":"2.0","result":{"experimentalApi":true,"grantedCapabilities":["sessionMcp","sessionListStream"],"museHome":"/tmp/muse-phase0/data/muse","platformFamily":"unix","platformOs":"linux","schema":{"fingerprint":"sha256:e0e163db6ccf00dbe68402ce55d6319b3edc33c421f31e9583b587b2de8a118f","version":1},"serverInfo":{"name":"muse","version":"1.4.1"},"sessionDurability":"durable","userAgent":"muse-build/1.4.1 (non-interactive; linux-x86_64; build 35815c253477406321e3f5595195becc25e3907a)"}}} +{"dir":"out","msg":{"jsonrpc":"2.0","method":"initialized","params":{}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":2,"method":"session/start","params":{"commandId":"01a0ee45-d4b5-7b36-adce-a396e7f9286f","workspaceRoot":"/tmp/muse-phase0/repo","modelId":"meta/muse-spark-1.3","providerId":"meta","approvalMode":"onRequest"}}} +{"dir":"in","msg":{"emittedAtMs":1790703949070,"jsonrpc":"2.0","method":"session/started","params":{"session":{"activeTurnId":null,"approvalMode":{"lastCommandId":null,"mode":"onRequest","source":"startup"},"createdAt":"2026-09-29T17:45:48.992273Z","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee45-d4bd-77d2-98a0-79c6a1333e80/session.jsonl","providerId":"meta","sessionId":"01a0ee45-d4bd-77d2-98a0-79c6a1333e80","status":"idle","turnCount":0,"updatedAt":"2026-09-29T17:45:48.992277Z","workspaceRoot":"/tmp/muse-phase0/repo"}}}} +{"dir":"in","msg":{"id":2,"jsonrpc":"2.0","result":{"session":{"activeTurnId":null,"approvalMode":{"lastCommandId":null,"mode":"onRequest","source":"startup"},"createdAt":"2026-09-29T17:45:48.992273Z","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee45-d4bd-77d2-98a0-79c6a1333e80/session.jsonl","providerId":"meta","sessionId":"01a0ee45-d4bd-77d2-98a0-79c6a1333e80","status":"idle","turnCount":0,"updatedAt":"2026-09-29T17:45:48.992277Z","workspaceRoot":"/tmp/muse-phase0/repo"},"viewCursor":"v:01a0ee45-d4bd-77d2-98a0-79c6a1333e80:1"}}} +{"dir":"in","msg":{"emittedAtMs":1790703949078,"jsonrpc":"2.0","method":"session/approvalModeChanged","params":{"clientName":"paseo_phase0","commandId":"01a0ee45-d4b5-7b36-adce-a396e7f9286f","mode":"onRequest","sessionId":"01a0ee45-d4bd-77d2-98a0-79c6a1333e80","source":"approvalReconfigure","sourceRange":{"first":{"id":"7ac702ca-483a-4aed-821f-2262a0f48943","sequence":5},"last":{"id":"7ac702ca-483a-4aed-821f-2262a0f48943","sequence":5},"stream":{"id":"01a0ee45-d4bd-77d2-98a0-79c6a1333e80","kind":"session"}},"viewCursor":"v:01a0ee45-d4bd-77d2-98a0-79c6a1333e80:1"}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":3,"method":"skill/list","params":{"sessionId":"01a0ee45-d4bd-77d2-98a0-79c6a1333e80"}}} +{"dir":"in","msg":{"id":3,"jsonrpc":"2.0","result":{"skills":[{"description":"Reply with the Phase 0 skill marker.","displayName":"phase0","selector":"phase0","source":"project"}]}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":4,"method":"turn/start","params":{"commandId":"01a0ee45-d51c-7adf-a4fc-c0c3e6b0a03e","sessionId":"01a0ee45-d4bd-77d2-98a0-79c6a1333e80","input":[{"type":"skill","selector":"phase0"}],"reasoningEffort":"low"}}} +{"dir":"in","msg":{"emittedAtMs":1790703949087,"jsonrpc":"2.0","method":"session/branchChanged","params":{"branch":"main","sessionId":"01a0ee45-d4bd-77d2-98a0-79c6a1333e80","sourceRange":{"first":{"id":"5ec98360-5a49-4413-bbdc-e423513d09ec","sequence":14},"last":{"id":"5ec98360-5a49-4413-bbdc-e423513d09ec","sequence":14},"stream":{"id":"01a0ee45-d4bd-77d2-98a0-79c6a1333e80","kind":"session"}},"vcs":"git","viewCursor":"v:01a0ee45-d4bd-77d2-98a0-79c6a1333e80:2","workspaceRoot":"/tmp/muse-phase0/repo"}}} +{"dir":"in","msg":{"id":4,"jsonrpc":"2.0","result":{"commandId":"01a0ee45-d51c-7adf-a4fc-c0c3e6b0a03e","disposition":"started","startedNewTurn":true,"status":"accepted","turnId":"01a0ee45-d51c-7adf-a4fc-c0c3e6b0a03e"}}} +{"dir":"in","msg":{"emittedAtMs":1790703949095,"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a0ee45-d4bd-77d2-98a0-79c6a1333e80","status":"running","viewCursor":"v:01a0ee45-d4bd-77d2-98a0-79c6a1333e80:3"}}} +{"dir":"in","msg":{"emittedAtMs":1790703949096,"jsonrpc":"2.0","method":"turn/started","params":{"commandId":"01a0ee45-d51c-7adf-a4fc-c0c3e6b0a03e","sessionId":"01a0ee45-d4bd-77d2-98a0-79c6a1333e80","sourceRange":{"first":{"id":"f8fbf545-2d43-4311-bf8c-dc367f670464","sequence":15},"last":{"id":"f8fbf545-2d43-4311-bf8c-dc367f670464","sequence":15},"stream":{"id":"01a0ee45-d4bd-77d2-98a0-79c6a1333e80","kind":"session"}},"turnId":"01a0ee45-d51c-7adf-a4fc-c0c3e6b0a03e","viewCursor":"v:01a0ee45-d4bd-77d2-98a0-79c6a1333e80:3"}}} +{"dir":"in","msg":{"emittedAtMs":1790703949096,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"commandId":"01a0ee45-d51c-7adf-a4fc-c0c3e6b0a03e","itemId":"5dbcaf15-bef5-49bf-8569-5242008ac198","kind":"userMessage","recordedAt":"2026-09-29T17:45:49.087991Z","revision":1,"status":"completed","text":"/phase0","turnId":"01a0ee45-d51c-7adf-a4fc-c0c3e6b0a03e"},"sessionId":"01a0ee45-d4bd-77d2-98a0-79c6a1333e80","sourceRange":{"first":{"id":"f8fbf545-2d43-4311-bf8c-dc367f670464","sequence":15},"last":{"id":"f8fbf545-2d43-4311-bf8c-dc367f670464","sequence":15},"stream":{"id":"01a0ee45-d4bd-77d2-98a0-79c6a1333e80","kind":"session"}},"viewCursor":"v:01a0ee45-d4bd-77d2-98a0-79c6a1333e80:4"}}} +{"dir":"in","msg":{"emittedAtMs":1790703949096,"jsonrpc":"2.0","method":"item/started","params":{"item":{"childSessionId":"24712ab9-77e4-450a-98b1-cfb455e64a99","fallbackText":"Reminder child session","generationId":1,"itemId":"3e9a8a07-f7ed-400f-aecd-45413565ff84","kind":"reminderChild","recordedAt":"2026-09-29T17:45:49.096505Z","reminderAgentId":"skill-reminder","revision":1,"status":"inProgress","taskId":"01a0ee45-d527-7133-8954-a78cbe078b40","turnId":"01a0ee45-d51c-7adf-a4fc-c0c3e6b0a03e"},"sessionId":"01a0ee45-d4bd-77d2-98a0-79c6a1333e80","sourceRange":{"first":{"id":"3e9a8a07-f7ed-400f-aecd-45413565ff84","sequence":34},"last":{"id":"3e9a8a07-f7ed-400f-aecd-45413565ff84","sequence":34},"stream":{"id":"01a0ee45-d4bd-77d2-98a0-79c6a1333e80","kind":"session"}},"viewCursor":"v:01a0ee45-d4bd-77d2-98a0-79c6a1333e80:5"}}} +{"dir":"in","msg":{"emittedAtMs":1790703949101,"jsonrpc":"2.0","method":"session/listChanged","params":{"session":{"activeTurnId":"01a0ee45-d51c-7adf-a4fc-c0c3e6b0a03e","createdAt":"2026-09-29T17:45:48.992273Z","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee45-d4bd-77d2-98a0-79c6a1333e80/session.jsonl","providerId":"meta","sessionId":"01a0ee45-d4bd-77d2-98a0-79c6a1333e80","status":"running","turnCount":0,"updatedAt":"2026-09-29T17:45:49.096262Z","workspaceRoot":"/tmp/muse-phase0/repo"}}}} +{"dir":"in","msg":{"emittedAtMs":1790703952151,"jsonrpc":"2.0","method":"item/started","params":{"item":{"itemId":"59a7cac6-9558-4ff9-a029-6871aa0f250f","kind":"agentMessage","revision":1,"status":"inProgress","text":"","turnId":"01a0ee45-d51c-7adf-a4fc-c0c3e6b0a03e"},"sessionId":"01a0ee45-d4bd-77d2-98a0-79c6a1333e80","viewCursor":"v:01a0ee45-d4bd-77d2-98a0-79c6a1333e80:5.1"}}} +{"dir":"in","msg":{"emittedAtMs":1790703952151,"jsonrpc":"2.0","method":"item/delta","params":{"delta":"SKILL_PHASE0","field":"text","itemId":"59a7cac6-9558-4ff9-a029-6871aa0f250f","sessionId":"01a0ee45-d4bd-77d2-98a0-79c6a1333e80","viewCursor":"v:01a0ee45-d4bd-77d2-98a0-79c6a1333e80:5.2"}}} +{"dir":"in","msg":{"emittedAtMs":1790703952151,"jsonrpc":"2.0","method":"item/delta","params":{"delta":"_OK","field":"text","itemId":"59a7cac6-9558-4ff9-a029-6871aa0f250f","sessionId":"01a0ee45-d4bd-77d2-98a0-79c6a1333e80","viewCursor":"v:01a0ee45-d4bd-77d2-98a0-79c6a1333e80:5.3"}}} +{"dir":"in","msg":{"emittedAtMs":1790703952208,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"itemId":"59a7cac6-9558-4ff9-a029-6871aa0f250f","kind":"agentMessage","recordedAt":"2026-09-29T17:45:52.208437Z","revision":2,"status":"completed","text":"SKILL_PHASE0_OK","turnId":"01a0ee45-d51c-7adf-a4fc-c0c3e6b0a03e"},"sessionId":"01a0ee45-d4bd-77d2-98a0-79c6a1333e80","sourceRange":{"first":{"id":"f6ce5178-aab1-4205-a5cf-07a01992991d","sequence":47},"last":{"id":"f6ce5178-aab1-4205-a5cf-07a01992991d","sequence":47},"stream":{"id":"01a0ee45-d4bd-77d2-98a0-79c6a1333e80","kind":"session"}},"viewCursor":"v:01a0ee45-d4bd-77d2-98a0-79c6a1333e80:6"}}} +{"dir":"in","msg":{"emittedAtMs":1790703952212,"jsonrpc":"2.0","method":"session/tokenUsage","params":{"cumulative":{"outputTokens":28,"promptTokens":38273,"totalTokens":38301},"durationMs":3111,"modelId":"meta/muse-spark-1.3","promptTokens":38273,"sessionId":"01a0ee45-d4bd-77d2-98a0-79c6a1333e80","sourceRange":{"first":{"id":"38e7ce9f-4de8-4e5b-9051-e4f3cbabeb07","sequence":50},"last":{"id":"38e7ce9f-4de8-4e5b-9051-e4f3cbabeb07","sequence":50},"stream":{"id":"01a0ee45-d4bd-77d2-98a0-79c6a1333e80","kind":"session"}},"totalTokens":38301,"turnId":"01a0ee45-d51c-7adf-a4fc-c0c3e6b0a03e","usage":{"cacheReadTokens":0,"cacheWriteTokens":0,"cachedTokens":0,"inputTokens":38273,"outputTokens":28,"reasoningTokens":13},"viewCursor":"v:01a0ee45-d4bd-77d2-98a0-79c6a1333e80:7"}}} +{"dir":"in","msg":{"emittedAtMs":1790703952214,"jsonrpc":"2.0","method":"item/started","params":{"item":{"childSessionId":"e36de071-1e04-472d-aa8a-8ceddb5de660","childSessionLogPath":"subagent/e36de071-1e04-472d-aa8a-8ceddb5de660/session.jsonl","fallbackText":"Reminder child session","generationId":1,"itemId":"c787e6af-1d6d-44b6-892e-2e8bc714b3b3","kind":"reminderChild","recordedAt":"2026-09-29T17:45:52.214263Z","reminderAgentId":"verify-reminder","revision":1,"status":"inProgress","taskId":"01a0ee45-e154-7511-a5d1-c318a04ffaca","turnId":"01a0ee45-d51c-7adf-a4fc-c0c3e6b0a03e"},"sessionId":"01a0ee45-d4bd-77d2-98a0-79c6a1333e80","sourceRange":{"first":{"id":"c787e6af-1d6d-44b6-892e-2e8bc714b3b3","sequence":57},"last":{"id":"c787e6af-1d6d-44b6-892e-2e8bc714b3b3","sequence":57},"stream":{"id":"01a0ee45-d4bd-77d2-98a0-79c6a1333e80","kind":"session"}},"viewCursor":"v:01a0ee45-d4bd-77d2-98a0-79c6a1333e80:8"}}} +{"dir":"in","msg":{"emittedAtMs":1790703958991,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"childSessionId":"24712ab9-77e4-450a-98b1-cfb455e64a99","fallbackText":"Reminder child session","generationId":1,"itemId":"3e9a8a07-f7ed-400f-aecd-45413565ff84","kind":"reminderChild","recordedAt":"2026-09-29T17:45:58.991191Z","reminderAgentId":"skill-reminder","revision":2,"status":"completed","taskId":"01a0ee45-d527-7133-8954-a78cbe078b40","turnId":"01a0ee45-d51c-7adf-a4fc-c0c3e6b0a03e"},"sessionId":"01a0ee45-d4bd-77d2-98a0-79c6a1333e80","sourceRange":{"first":{"id":"3e9a8a07-f7ed-400f-aecd-45413565ff84","sequence":34},"last":{"id":"f5852563-4e6c-497f-a014-b9f510fd8a08","sequence":59},"stream":{"id":"01a0ee45-d4bd-77d2-98a0-79c6a1333e80","kind":"session"}},"viewCursor":"v:01a0ee45-d4bd-77d2-98a0-79c6a1333e80:9"}}} +{"dir":"in","msg":{"emittedAtMs":1790703958991,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"childSessionId":"e36de071-1e04-472d-aa8a-8ceddb5de660","childSessionLogPath":"subagent/e36de071-1e04-472d-aa8a-8ceddb5de660/session.jsonl","fallbackText":"Reminder child session","generationId":1,"itemId":"c787e6af-1d6d-44b6-892e-2e8bc714b3b3","kind":"reminderChild","recordedAt":"2026-09-29T17:45:58.991586Z","reminderAgentId":"verify-reminder","revision":2,"status":"completed","taskId":"01a0ee45-e154-7511-a5d1-c318a04ffaca","turnId":"01a0ee45-d51c-7adf-a4fc-c0c3e6b0a03e"},"sessionId":"01a0ee45-d4bd-77d2-98a0-79c6a1333e80","sourceRange":{"first":{"id":"c787e6af-1d6d-44b6-892e-2e8bc714b3b3","sequence":57},"last":{"id":"49c39143-103a-4ff6-9da9-5f460199aad5","sequence":60},"stream":{"id":"01a0ee45-d4bd-77d2-98a0-79c6a1333e80","kind":"session"}},"viewCursor":"v:01a0ee45-d4bd-77d2-98a0-79c6a1333e80:10"}}} +{"dir":"in","msg":{"emittedAtMs":1790703958993,"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a0ee45-d4bd-77d2-98a0-79c6a1333e80","status":"idle","viewCursor":"v:01a0ee45-d4bd-77d2-98a0-79c6a1333e80:11"}}} +{"dir":"in","msg":{"emittedAtMs":1790703958994,"jsonrpc":"2.0","method":"turn/completed","params":{"durationMs":9905,"sessionId":"01a0ee45-d4bd-77d2-98a0-79c6a1333e80","sourceRange":{"first":{"id":"0085e82d-1273-4721-a949-b0616fc6e059","sequence":66},"last":{"id":"0085e82d-1273-4721-a949-b0616fc6e059","sequence":66},"stream":{"id":"01a0ee45-d4bd-77d2-98a0-79c6a1333e80","kind":"session"}},"terminal":"completed","timeToFirstTokenMs":3063,"turnId":"01a0ee45-d51c-7adf-a4fc-c0c3e6b0a03e","viewCursor":"v:01a0ee45-d4bd-77d2-98a0-79c6a1333e80:11"}}} +{"dir":"in","msg":{"emittedAtMs":1790703958994,"jsonrpc":"2.0","method":"session/listChanged","params":{"session":{"activeTurnId":null,"branch":"main","createdAt":"2026-09-29T17:45:48.989825Z","firstUserPrompt":"/phase0","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee45-d4bd-77d2-98a0-79c6a1333e80/session.jsonl","providerId":"meta","sessionId":"01a0ee45-d4bd-77d2-98a0-79c6a1333e80","status":"idle","title":"/phase0","turnCount":1,"updatedAt":"2026-09-29T17:45:58.993139Z","workspaceRoot":"/tmp/muse-phase0/repo"}}}} +{"dir":"in","msg":{"emittedAtMs":1790703959207,"jsonrpc":"2.0","method":"session/closed","params":{"reason":"hostShutdown","sessionId":"01a0ee45-d4bd-77d2-98a0-79c6a1333e80","viewCursor":"v:01a0ee45-d4bd-77d2-98a0-79c6a1333e80:12"}}} +{"dir":"in","msg":{"emittedAtMs":1790703959207,"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a0ee45-d4bd-77d2-98a0-79c6a1333e80","status":"notLoaded","viewCursor":"v:01a0ee45-d4bd-77d2-98a0-79c6a1333e80:12"}}} +{"dir":"in","msg":{"emittedAtMs":1790703959207,"jsonrpc":"2.0","method":"session/listChanged","params":{"session":{"activeTurnId":null,"branch":"main","createdAt":"2026-09-29T17:45:48.989825Z","firstUserPrompt":"/phase0","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee45-d4bd-77d2-98a0-79c6a1333e80/session.jsonl","providerId":"meta","sessionId":"01a0ee45-d4bd-77d2-98a0-79c6a1333e80","status":"notLoaded","title":"/phase0","turnCount":1,"updatedAt":"2026-09-29T17:45:58.994348Z","workspaceRoot":"/tmp/muse-phase0/repo"}}}} diff --git a/plugins/muse-provider/test/fixtures/staged-approval.ndjson b/plugins/muse-provider/test/fixtures/staged-approval.ndjson new file mode 100644 index 0000000000..6c64f9be72 --- /dev/null +++ b/plugins/muse-provider/test/fixtures/staged-approval.ndjson @@ -0,0 +1,51 @@ +{"dir":"out","msg":{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"clientInfo":{"name":"paseo_phase0","version":"0.0.0"},"capabilities":{"experimentalApi":true,"requestedCapabilities":["sessionMcp","sessionListStream"]}}}} +{"dir":"in","msg":{"id":1,"jsonrpc":"2.0","result":{"experimentalApi":true,"grantedCapabilities":["sessionMcp","sessionListStream"],"museHome":"/tmp/muse-phase0/data/muse","platformFamily":"unix","platformOs":"linux","schema":{"fingerprint":"sha256:e0e163db6ccf00dbe68402ce55d6319b3edc33c421f31e9583b587b2de8a118f","version":1},"serverInfo":{"name":"muse","version":"1.4.1"},"sessionDurability":"durable","userAgent":"muse-build/1.4.1 (non-interactive; linux-x86_64; build 35815c253477406321e3f5595195becc25e3907a)"}}} +{"dir":"out","msg":{"jsonrpc":"2.0","method":"initialized","params":{}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":2,"method":"session/start","params":{"commandId":"01a0ee45-3a70-7681-ab97-ddf447f594a0","workspaceRoot":"/tmp/muse-phase0/repo","modelId":"meta/muse-spark-1.3","providerId":"meta","approvalMode":"onRequest"}}} +{"dir":"in","msg":{"emittedAtMs":1790703909575,"jsonrpc":"2.0","method":"session/started","params":{"session":{"activeTurnId":null,"approvalMode":{"lastCommandId":null,"mode":"onRequest","source":"startup"},"createdAt":"2026-09-29T17:45:09.4981Z","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee45-3a77-7df1-84fe-f1348f74bb7b/session.jsonl","providerId":"meta","sessionId":"01a0ee45-3a77-7df1-84fe-f1348f74bb7b","status":"idle","turnCount":0,"updatedAt":"2026-09-29T17:45:09.498104Z","workspaceRoot":"/tmp/muse-phase0/repo"}}}} +{"dir":"in","msg":{"id":2,"jsonrpc":"2.0","result":{"session":{"activeTurnId":null,"approvalMode":{"lastCommandId":null,"mode":"onRequest","source":"startup"},"createdAt":"2026-09-29T17:45:09.4981Z","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee45-3a77-7df1-84fe-f1348f74bb7b/session.jsonl","providerId":"meta","sessionId":"01a0ee45-3a77-7df1-84fe-f1348f74bb7b","status":"idle","turnCount":0,"updatedAt":"2026-09-29T17:45:09.498104Z","workspaceRoot":"/tmp/muse-phase0/repo"},"viewCursor":"v:01a0ee45-3a77-7df1-84fe-f1348f74bb7b:1"}}} +{"dir":"in","msg":{"emittedAtMs":1790703909580,"jsonrpc":"2.0","method":"session/approvalModeChanged","params":{"clientName":"paseo_phase0","commandId":"01a0ee45-3a70-7681-ab97-ddf447f594a0","mode":"onRequest","sessionId":"01a0ee45-3a77-7df1-84fe-f1348f74bb7b","source":"approvalReconfigure","sourceRange":{"first":{"id":"63699ada-6377-4996-8f16-591c59e31615","sequence":5},"last":{"id":"63699ada-6377-4996-8f16-591c59e31615","sequence":5},"stream":{"id":"01a0ee45-3a77-7df1-84fe-f1348f74bb7b","kind":"session"}},"viewCursor":"v:01a0ee45-3a77-7df1-84fe-f1348f74bb7b:1"}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":3,"method":"turn/start","params":{"commandId":"01a0ee45-3acc-7f17-a996-8de3b5463c29","sessionId":"01a0ee45-3a77-7df1-84fe-f1348f74bb7b","input":[{"type":"text","text":"Run exactly wc -l f && rm -rf f using the shell tool. f is disposable test data and I authorize deleting it. Do not split the command into separate calls."}],"reasoningEffort":"low"}}} +{"dir":"in","msg":{"emittedAtMs":1790703909582,"jsonrpc":"2.0","method":"session/branchChanged","params":{"branch":"main","sessionId":"01a0ee45-3a77-7df1-84fe-f1348f74bb7b","sourceRange":{"first":{"id":"f5b00d5c-101c-4114-b6d4-6fd185b3a9e7","sequence":15},"last":{"id":"f5b00d5c-101c-4114-b6d4-6fd185b3a9e7","sequence":15},"stream":{"id":"01a0ee45-3a77-7df1-84fe-f1348f74bb7b","kind":"session"}},"vcs":"git","viewCursor":"v:01a0ee45-3a77-7df1-84fe-f1348f74bb7b:2","workspaceRoot":"/tmp/muse-phase0/repo"}}} +{"dir":"in","msg":{"id":3,"jsonrpc":"2.0","result":{"commandId":"01a0ee45-3acc-7f17-a996-8de3b5463c29","disposition":"started","startedNewTurn":true,"status":"accepted","turnId":"01a0ee45-3acc-7f17-a996-8de3b5463c29"}}} +{"dir":"in","msg":{"emittedAtMs":1790703909590,"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a0ee45-3a77-7df1-84fe-f1348f74bb7b","status":"running","viewCursor":"v:01a0ee45-3a77-7df1-84fe-f1348f74bb7b:3"}}} +{"dir":"in","msg":{"emittedAtMs":1790703909591,"jsonrpc":"2.0","method":"turn/started","params":{"commandId":"01a0ee45-3acc-7f17-a996-8de3b5463c29","sessionId":"01a0ee45-3a77-7df1-84fe-f1348f74bb7b","sourceRange":{"first":{"id":"8d53885c-d5ed-4da6-826f-c2718d3107ba","sequence":17},"last":{"id":"8d53885c-d5ed-4da6-826f-c2718d3107ba","sequence":17},"stream":{"id":"01a0ee45-3a77-7df1-84fe-f1348f74bb7b","kind":"session"}},"turnId":"01a0ee45-3acc-7f17-a996-8de3b5463c29","viewCursor":"v:01a0ee45-3a77-7df1-84fe-f1348f74bb7b:3"}}} +{"dir":"in","msg":{"emittedAtMs":1790703909591,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"commandId":"01a0ee45-3acc-7f17-a996-8de3b5463c29","itemId":"c1c58c19-3efe-486b-83b4-7059c81e4abc","kind":"userMessage","recordedAt":"2026-09-29T17:45:09.583635Z","revision":1,"status":"completed","text":"Run exactly wc -l f && rm -rf f using the shell tool. f is disposable test data and I authorize deleting it. Do not split the command into separate calls.","turnId":"01a0ee45-3acc-7f17-a996-8de3b5463c29"},"sessionId":"01a0ee45-3a77-7df1-84fe-f1348f74bb7b","sourceRange":{"first":{"id":"8d53885c-d5ed-4da6-826f-c2718d3107ba","sequence":17},"last":{"id":"8d53885c-d5ed-4da6-826f-c2718d3107ba","sequence":17},"stream":{"id":"01a0ee45-3a77-7df1-84fe-f1348f74bb7b","kind":"session"}},"viewCursor":"v:01a0ee45-3a77-7df1-84fe-f1348f74bb7b:4"}}} +{"dir":"in","msg":{"emittedAtMs":1790703909591,"jsonrpc":"2.0","method":"item/started","params":{"item":{"childSessionId":"8d237cca-8a96-414d-b5a0-b922372b1111","fallbackText":"Reminder child session","generationId":1,"itemId":"604107b6-dcf2-4ac8-9c20-85adace9a056","kind":"reminderChild","recordedAt":"2026-09-29T17:45:09.590882Z","reminderAgentId":"skill-reminder","revision":1,"status":"inProgress","taskId":"01a0ee45-3ad5-7ae3-b92b-6080ffbaed08","turnId":"01a0ee45-3acc-7f17-a996-8de3b5463c29"},"sessionId":"01a0ee45-3a77-7df1-84fe-f1348f74bb7b","sourceRange":{"first":{"id":"604107b6-dcf2-4ac8-9c20-85adace9a056","sequence":35},"last":{"id":"604107b6-dcf2-4ac8-9c20-85adace9a056","sequence":35},"stream":{"id":"01a0ee45-3a77-7df1-84fe-f1348f74bb7b","kind":"session"}},"viewCursor":"v:01a0ee45-3a77-7df1-84fe-f1348f74bb7b:5"}}} +{"dir":"in","msg":{"emittedAtMs":1790703909595,"jsonrpc":"2.0","method":"session/listChanged","params":{"session":{"activeTurnId":"01a0ee45-3acc-7f17-a996-8de3b5463c29","createdAt":"2026-09-29T17:45:09.4981Z","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee45-3a77-7df1-84fe-f1348f74bb7b/session.jsonl","providerId":"meta","sessionId":"01a0ee45-3a77-7df1-84fe-f1348f74bb7b","status":"running","turnCount":0,"updatedAt":"2026-09-29T17:45:09.591134Z","workspaceRoot":"/tmp/muse-phase0/repo"}}}} +{"dir":"in","msg":{"emittedAtMs":1790703915208,"jsonrpc":"2.0","method":"session/tokenUsage","params":{"cumulative":{"outputTokens":301,"promptTokens":38156,"totalTokens":38457},"durationMs":5613,"finishReason":"tool_calls","modelId":"meta/muse-spark-1.3","promptTokens":38156,"sessionId":"01a0ee45-3a77-7df1-84fe-f1348f74bb7b","sourceRange":{"first":{"id":"adcc2a9f-a1ab-428f-b213-1c3dc1d19282","sequence":51},"last":{"id":"adcc2a9f-a1ab-428f-b213-1c3dc1d19282","sequence":51},"stream":{"id":"01a0ee45-3a77-7df1-84fe-f1348f74bb7b","kind":"session"}},"totalTokens":38457,"turnId":"01a0ee45-3acc-7f17-a996-8de3b5463c29","usage":{"cacheReadTokens":0,"cacheWriteTokens":0,"cachedTokens":0,"inputTokens":38156,"outputTokens":301,"reasoningTokens":201},"viewCursor":"v:01a0ee45-3a77-7df1-84fe-f1348f74bb7b:6"}}} +{"dir":"in","msg":{"emittedAtMs":1790703915210,"jsonrpc":"2.0","method":"session/statusChanged","params":{"attention":["approvalPending"],"sessionId":"01a0ee45-3a77-7df1-84fe-f1348f74bb7b","status":"running","viewCursor":"v:01a0ee45-3a77-7df1-84fe-f1348f74bb7b:7"}}} +{"dir":"in","msg":{"id":1,"jsonrpc":"2.0","method":"approval/request","params":{"approvalId":"01a0ee45-50c8-7b92-a88f-04a4dfa119a5","availableChoices":[{"choiceId":"allow_once","decision":"approved","label":"Allow once","scope":"once"},{"choiceId":"allow_local_prefix","decision":"approvedPolicyAmendment","label":"Always allow in this workspace: rm ...","rulePreview":"Always allow in this workspace: rm ...","scope":"localPersistent"},{"acceptsFeedback":true,"choiceId":"abort","decision":"abort","label":"Reject","scope":"once"}],"currentRequirementId":{"approvalId":"01a0ee45-50c8-7b92-a88f-04a4dfa119a5","sourceIndex":1},"itemId":"01a0ee45-50c8-7b92-a88f-04a4dfa119a5","judgeEscalated":false,"protectedWrite":false,"rawArgs":"{\"command\":\"wc -l f && rm -rf f\",\"description\":\"Count lines then remove file\"}","sessionId":"01a0ee45-3a77-7df1-84fe-f1348f74bb7b","sourceRange":{"first":{"id":"5e822176-0464-43c4-9419-4355b481f8a2","sequence":56},"last":{"id":"5e822176-0464-43c4-9419-4355b481f8a2","sequence":56},"stream":{"id":"01a0ee45-3a77-7df1-84fe-f1348f74bb7b","kind":"session"}},"subject":{"command":"wc -l f && rm -rf f","kind":"shell","stages":[{"argv":["wc","-l","f"],"argvComplete":true,"position":1,"requirementId":{"approvalId":"01a0ee45-50c8-7b92-a88f-04a4dfa119a5","sourceIndex":0},"resolution":{"kind":"knownSafe"},"suggestedPrefix":{"argvPrefix":["wc"],"label":"Always allow in this workspace: wc ..."},"totalStages":2},{"argv":["rm","-rf","f"],"argvComplete":true,"position":2,"requirementId":{"approvalId":"01a0ee45-50c8-7b92-a88f-04a4dfa119a5","sourceIndex":1},"resolution":{"kind":"unresolved"},"suggestedPrefix":{"argvPrefix":["rm"],"label":"Always allow in this workspace: rm ..."},"totalStages":2}],"workspaceRoot":"/tmp/muse-phase0/repo"},"taskId":"01a0ee45-50c8-7b92-a88f-04a4dfa119a5","toolCallId":"call_01a0ee454ed276abb13fb5bc95951f53","toolName":"bash","turnId":"01a0ee45-3acc-7f17-a996-8de3b5463c29","viewCursor":"v:01a0ee45-3a77-7df1-84fe-f1348f74bb7b:7"}}} +{"dir":"in","msg":{"emittedAtMs":1790703915211,"jsonrpc":"2.0","method":"approval/requested","params":{"approvalId":"01a0ee45-50c8-7b92-a88f-04a4dfa119a5","availableChoices":[{"choiceId":"allow_once","decision":"approved","label":"Allow once","scope":"once"},{"choiceId":"allow_local_prefix","decision":"approvedPolicyAmendment","label":"Always allow in this workspace: rm ...","rulePreview":"Always allow in this workspace: rm ...","scope":"localPersistent"},{"acceptsFeedback":true,"choiceId":"abort","decision":"abort","label":"Reject","scope":"once"}],"currentRequirementId":{"approvalId":"01a0ee45-50c8-7b92-a88f-04a4dfa119a5","sourceIndex":1},"itemId":"01a0ee45-50c8-7b92-a88f-04a4dfa119a5","judgeEscalated":false,"protectedWrite":false,"rawArgs":"{\"command\":\"wc -l f && rm -rf f\",\"description\":\"Count lines then remove file\"}","sessionId":"01a0ee45-3a77-7df1-84fe-f1348f74bb7b","sourceRange":{"first":{"id":"5e822176-0464-43c4-9419-4355b481f8a2","sequence":56},"last":{"id":"5e822176-0464-43c4-9419-4355b481f8a2","sequence":56},"stream":{"id":"01a0ee45-3a77-7df1-84fe-f1348f74bb7b","kind":"session"}},"subject":{"command":"wc -l f && rm -rf f","kind":"shell","stages":[{"argv":["wc","-l","f"],"argvComplete":true,"position":1,"requirementId":{"approvalId":"01a0ee45-50c8-7b92-a88f-04a4dfa119a5","sourceIndex":0},"resolution":{"kind":"knownSafe"},"suggestedPrefix":{"argvPrefix":["wc"],"label":"Always allow in this workspace: wc ..."},"totalStages":2},{"argv":["rm","-rf","f"],"argvComplete":true,"position":2,"requirementId":{"approvalId":"01a0ee45-50c8-7b92-a88f-04a4dfa119a5","sourceIndex":1},"resolution":{"kind":"unresolved"},"suggestedPrefix":{"argvPrefix":["rm"],"label":"Always allow in this workspace: rm ..."},"totalStages":2}],"workspaceRoot":"/tmp/muse-phase0/repo"},"taskId":"01a0ee45-50c8-7b92-a88f-04a4dfa119a5","toolCallId":"call_01a0ee454ed276abb13fb5bc95951f53","toolName":"bash","turnId":"01a0ee45-3acc-7f17-a996-8de3b5463c29","viewCursor":"v:01a0ee45-3a77-7df1-84fe-f1348f74bb7b:7"}}} +{"dir":"in","msg":{"emittedAtMs":1790703915211,"jsonrpc":"2.0","method":"session/listChanged","params":{"session":{"activeTurnId":"01a0ee45-3acc-7f17-a996-8de3b5463c29","attention":["approvalPending"],"branch":"main","createdAt":"2026-09-29T17:45:09.495708Z","firstUserPrompt":"Run exactly wc -l f && rm -rf f using the shell tool. f is disposable test data and I authorize deleting it. Do not split the command into separate calls.","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee45-3a77-7df1-84fe-f1348f74bb7b/session.jsonl","providerId":"meta","sessionId":"01a0ee45-3a77-7df1-84fe-f1348f74bb7b","status":"running","title":"Run exactly wc -l f && rm -rf f using the shell tool. f is disposable test data and I authorize deleting it. Do not split the command into separate calls.","turnCount":0,"updatedAt":"2026-09-29T17:45:15.210247Z","workspaceRoot":"/tmp/muse-phase0/repo"}}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":1,"result":{}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":4,"method":"approval/decide","params":{"commandId":"01a0ee45-50cb-7347-a392-c2c3f12add59","sessionId":"01a0ee45-3a77-7df1-84fe-f1348f74bb7b","approvalId":"01a0ee45-50c8-7b92-a88f-04a4dfa119a5","requirementId":{"approvalId":"01a0ee45-50c8-7b92-a88f-04a4dfa119a5","sourceIndex":1},"choiceId":"allow_once"}}} +{"dir":"in","msg":{"id":4,"jsonrpc":"2.0","result":{"approvalId":"01a0ee45-50c8-7b92-a88f-04a4dfa119a5","commandId":"01a0ee45-50cb-7347-a392-c2c3f12add59","status":"accepted","terminal":true}}} +{"dir":"in","msg":{"emittedAtMs":1790703915221,"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a0ee45-3a77-7df1-84fe-f1348f74bb7b","status":"running","viewCursor":"v:01a0ee45-3a77-7df1-84fe-f1348f74bb7b:9"}}} +{"dir":"in","msg":{"emittedAtMs":1790703915221,"jsonrpc":"2.0","method":"approval/updated","params":{"approvalId":"01a0ee45-50c8-7b92-a88f-04a4dfa119a5","availableChoices":[{"choiceId":"allow_once","decision":"approved","label":"Allow once","scope":"once"},{"choiceId":"allow_local_prefix","decision":"approvedPolicyAmendment","label":"Always allow in this workspace: rm ...","rulePreview":"Always allow in this workspace: rm ...","scope":"localPersistent"},{"acceptsFeedback":true,"choiceId":"abort","decision":"abort","label":"Reject","scope":"once"}],"change":{"choiceId":"allow_once","decision":"approved","kind":"stageResolved","requirementId":{"approvalId":"01a0ee45-50c8-7b92-a88f-04a4dfa119a5","sourceIndex":1}},"currentRequirementId":{"approvalId":"01a0ee45-50c8-7b92-a88f-04a4dfa119a5","sourceIndex":1},"sessionId":"01a0ee45-3a77-7df1-84fe-f1348f74bb7b","sourceRange":{"first":{"id":"8fb0dc1b-0325-49f0-90c7-127e642863ba","sequence":59},"last":{"id":"8fb0dc1b-0325-49f0-90c7-127e642863ba","sequence":59},"stream":{"id":"01a0ee45-3a77-7df1-84fe-f1348f74bb7b","kind":"session"}},"subject":{"command":"wc -l f && rm -rf f","kind":"shell","stages":[{"argv":["wc","-l","f"],"argvComplete":true,"position":1,"requirementId":{"approvalId":"01a0ee45-50c8-7b92-a88f-04a4dfa119a5","sourceIndex":0},"resolution":{"kind":"knownSafe"},"suggestedPrefix":{"argvPrefix":["wc"],"label":"Always allow in this workspace: wc ..."},"totalStages":2},{"argv":["rm","-rf","f"],"argvComplete":true,"position":2,"requirementId":{"approvalId":"01a0ee45-50c8-7b92-a88f-04a4dfa119a5","sourceIndex":1},"resolution":{"kind":"allowOnce"},"suggestedPrefix":{"argvPrefix":["rm"],"label":"Always allow in this workspace: rm ..."},"totalStages":2}],"workspaceRoot":"/tmp/muse-phase0/repo"},"viewCursor":"v:01a0ee45-3a77-7df1-84fe-f1348f74bb7b:8"}}} +{"dir":"in","msg":{"emittedAtMs":1790703915221,"jsonrpc":"2.0","method":"approval/resolved","params":{"approvalId":"01a0ee45-50c8-7b92-a88f-04a4dfa119a5","decidedByCommandId":"01a0ee45-50cb-7347-a392-c2c3f12add59","decision":"approved","itemId":"01a0ee45-50c8-7b92-a88f-04a4dfa119a5","policyResult":"allow","resolvedBy":"user","sessionId":"01a0ee45-3a77-7df1-84fe-f1348f74bb7b","sourceRange":{"first":{"id":"6b80bd39-abfc-45fb-9c9b-4dd995ede7af","sequence":60},"last":{"id":"6b80bd39-abfc-45fb-9c9b-4dd995ede7af","sequence":60},"stream":{"id":"01a0ee45-3a77-7df1-84fe-f1348f74bb7b","kind":"session"}},"stageEvidence":[{"argv":["wc","-l","f"],"position":1,"requirementId":{"approvalId":"01a0ee45-50c8-7b92-a88f-04a4dfa119a5","sourceIndex":0},"resolution":{"kind":"knownSafe"},"totalStages":2},{"argv":["rm","-rf","f"],"position":2,"requirementId":{"approvalId":"01a0ee45-50c8-7b92-a88f-04a4dfa119a5","sourceIndex":1},"resolution":{"kind":"allowOnce"},"totalStages":2}],"turnId":"01a0ee45-3acc-7f17-a996-8de3b5463c29","viewCursor":"v:01a0ee45-3a77-7df1-84fe-f1348f74bb7b:9"}}} +{"dir":"in","msg":{"emittedAtMs":1790703915221,"jsonrpc":"2.0","method":"item/started","params":{"item":{"args":"{\"command\":\"wc -l f && rm -rf f\",\"description\":\"Count lines then remove file\"}","callId":"call_01a0ee454ed276abb13fb5bc95951f53","itemId":"01a0ee45-50c8-7b92-a88f-04a4dfa119a5","kind":"toolCall","recordedAt":"2026-09-29T17:45:15.217346Z","revision":1,"status":"inProgress","tool":"bash","turnId":"01a0ee45-3acc-7f17-a996-8de3b5463c29"},"sessionId":"01a0ee45-3a77-7df1-84fe-f1348f74bb7b","sourceRange":{"first":{"id":"a1c326a1-b2f7-460d-9bf0-42fbc0e7b995","sequence":53},"last":{"id":"47723f89-a36b-4c4d-8011-63ae936b976a","sequence":66},"stream":{"id":"01a0ee45-3a77-7df1-84fe-f1348f74bb7b","kind":"session"}},"viewCursor":"v:01a0ee45-3a77-7df1-84fe-f1348f74bb7b:10"}}} +{"dir":"in","msg":{"emittedAtMs":1790703915221,"jsonrpc":"2.0","method":"session/listChanged","params":{"session":{"activeTurnId":"01a0ee45-3acc-7f17-a996-8de3b5463c29","branch":"main","createdAt":"2026-09-29T17:45:09.495708Z","firstUserPrompt":"Run exactly wc -l f && rm -rf f using the shell tool. f is disposable test data and I authorize deleting it. Do not split the command into separate calls.","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee45-3a77-7df1-84fe-f1348f74bb7b/session.jsonl","providerId":"meta","sessionId":"01a0ee45-3a77-7df1-84fe-f1348f74bb7b","status":"running","title":"Run exactly wc -l f && rm -rf f using the shell tool. f is disposable test data and I authorize deleting it. Do not split the command into separate calls.","turnCount":0,"updatedAt":"2026-09-29T17:45:15.21762Z","workspaceRoot":"/tmp/muse-phase0/repo"}}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":5,"method":"approval/decide","params":{"commandId":"01a0ee45-50d5-73ff-a6a1-b8db5e9cdf2f","sessionId":"01a0ee45-3a77-7df1-84fe-f1348f74bb7b","approvalId":"01a0ee45-50c8-7b92-a88f-04a4dfa119a5","requirementId":{"approvalId":"01a0ee45-50c8-7b92-a88f-04a4dfa119a5","sourceIndex":1},"choiceId":"allow_once"}}} +{"dir":"in","msg":{"error":{"code":-32051,"data":{"approvalId":"01a0ee45-50c8-7b92-a88f-04a4dfa119a5","kind":"approvalAlreadyResolved","resolution":{"decision":"approved","resolvedBy":"user","viewCursor":"v:01a0ee45-3a77-7df1-84fe-f1348f74bb7b:9"},"retryable":false},"message":"approval 01a0ee45-50c8-7b92-a88f-04a4dfa119a5 is already resolved"},"id":5,"jsonrpc":"2.0"}} +{"dir":"in","msg":{"emittedAtMs":1790703915429,"jsonrpc":"2.0","method":"item/delta","params":{"delta":"2 f\n","field":"output","itemId":"01a0ee45-50c8-7b92-a88f-04a4dfa119a5","sessionId":"01a0ee45-3a77-7df1-84fe-f1348f74bb7b","viewCursor":"v:01a0ee45-3a77-7df1-84fe-f1348f74bb7b:10.1"}}} +{"dir":"in","msg":{"emittedAtMs":1790703915442,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"args":"{\"command\":\"wc -l f && rm -rf f\",\"description\":\"Count lines then remove file\"}","callId":"call_01a0ee454ed276abb13fb5bc95951f53","itemId":"01a0ee45-50c8-7b92-a88f-04a4dfa119a5","kind":"toolCall","recordedAt":"2026-09-29T17:45:15.442455Z","revision":2,"status":"completed","tool":"bash","turnId":"01a0ee45-3acc-7f17-a996-8de3b5463c29","visibleOutput":"2 f\n"},"sessionId":"01a0ee45-3a77-7df1-84fe-f1348f74bb7b","sourceRange":{"first":{"id":"a1c326a1-b2f7-460d-9bf0-42fbc0e7b995","sequence":53},"last":{"id":"d11e7d23-dde1-48ac-8775-6afbc5b1eaa4","sequence":74},"stream":{"id":"01a0ee45-3a77-7df1-84fe-f1348f74bb7b","kind":"session"}},"viewCursor":"v:01a0ee45-3a77-7df1-84fe-f1348f74bb7b:11"}}} +{"dir":"in","msg":{"emittedAtMs":1790703915444,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"childSessionId":"8d237cca-8a96-414d-b5a0-b922372b1111","fallbackText":"Reminder child session","generationId":1,"itemId":"604107b6-dcf2-4ac8-9c20-85adace9a056","kind":"reminderChild","recordedAt":"2026-09-29T17:45:15.444591Z","reminderAgentId":"skill-reminder","revision":2,"status":"completed","taskId":"01a0ee45-3ad5-7ae3-b92b-6080ffbaed08","turnId":"01a0ee45-3acc-7f17-a996-8de3b5463c29"},"sessionId":"01a0ee45-3a77-7df1-84fe-f1348f74bb7b","sourceRange":{"first":{"id":"604107b6-dcf2-4ac8-9c20-85adace9a056","sequence":35},"last":{"id":"8db47bbe-b881-4efb-8e30-4838586ce540","sequence":77},"stream":{"id":"01a0ee45-3a77-7df1-84fe-f1348f74bb7b","kind":"session"}},"viewCursor":"v:01a0ee45-3a77-7df1-84fe-f1348f74bb7b:12"}}} +{"dir":"in","msg":{"emittedAtMs":1790703915447,"jsonrpc":"2.0","method":"item/started","params":{"item":{"childSessionId":"d461ee5e-a0e2-482d-a91c-604217650af2","fallbackText":"Reminder child session","generationId":2,"itemId":"553828b6-6f65-405e-b5d9-06e5a9ad24e9","kind":"reminderChild","recordedAt":"2026-09-29T17:45:15.447283Z","reminderAgentId":"skill-reminder","revision":1,"status":"inProgress","taskId":"01a0ee45-51b5-78a1-bd60-165e8384cd55","turnId":"01a0ee45-3acc-7f17-a996-8de3b5463c29"},"sessionId":"01a0ee45-3a77-7df1-84fe-f1348f74bb7b","sourceRange":{"first":{"id":"553828b6-6f65-405e-b5d9-06e5a9ad24e9","sequence":86},"last":{"id":"553828b6-6f65-405e-b5d9-06e5a9ad24e9","sequence":86},"stream":{"id":"01a0ee45-3a77-7df1-84fe-f1348f74bb7b","kind":"session"}},"viewCursor":"v:01a0ee45-3a77-7df1-84fe-f1348f74bb7b:13"}}} +{"dir":"in","msg":{"emittedAtMs":1790703918084,"jsonrpc":"2.0","method":"item/started","params":{"item":{"itemId":"619a0511-ea64-4544-93c9-897f38af1eff","kind":"agentMessage","revision":1,"status":"inProgress","text":"","turnId":"01a0ee45-3acc-7f17-a996-8de3b5463c29"},"sessionId":"01a0ee45-3a77-7df1-84fe-f1348f74bb7b","viewCursor":"v:01a0ee45-3a77-7df1-84fe-f1348f74bb7b:13.1"}}} +{"dir":"in","msg":{"emittedAtMs":1790703918084,"jsonrpc":"2.0","method":"item/delta","params":{"delta":"Ran `wc -l f &&","field":"text","itemId":"619a0511-ea64-4544-93c9-897f38af1eff","sessionId":"01a0ee45-3a77-7df1-84fe-f1348f74bb7b","viewCursor":"v:01a0ee45-3a77-7df1-84fe-f1348f74bb7b:13.2"}}} +{"dir":"in","msg":{"emittedAtMs":1790703918084,"jsonrpc":"2.0","method":"item/delta","params":{"delta":" rm -rf f` in one call.","field":"text","itemId":"619a0511-ea64-4544-93c9-897f38af1eff","sessionId":"01a0ee45-3a77-7df1-84fe-f1348f74bb7b","viewCursor":"v:01a0ee45-3a77-7df1-84fe-f1348f74bb7b:13.3"}}} +{"dir":"in","msg":{"emittedAtMs":1790703918084,"jsonrpc":"2.0","method":"item/delta","params":{"delta":" Output:","field":"text","itemId":"619a0511-ea64-4544-93c9-897f38af1eff","sessionId":"01a0ee45-3a77-7df1-84fe-f1348f74bb7b","viewCursor":"v:01a0ee45-3a77-7df1-84fe-f1348f74bb7b:13.4"}}} +{"dir":"in","msg":{"emittedAtMs":1790703918084,"jsonrpc":"2.0","method":"item/delta","params":{"delta":" `2 f`, file removed","field":"text","itemId":"619a0511-ea64-4544-93c9-897f38af1eff","sessionId":"01a0ee45-3a77-7df1-84fe-f1348f74bb7b","viewCursor":"v:01a0ee45-3a77-7df1-84fe-f1348f74bb7b:13.5"}}} +{"dir":"in","msg":{"emittedAtMs":1790703918087,"jsonrpc":"2.0","method":"item/delta","params":{"delta":".","field":"text","itemId":"619a0511-ea64-4544-93c9-897f38af1eff","sessionId":"01a0ee45-3a77-7df1-84fe-f1348f74bb7b","viewCursor":"v:01a0ee45-3a77-7df1-84fe-f1348f74bb7b:13.6"}}} +{"dir":"in","msg":{"emittedAtMs":1790703918160,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"itemId":"619a0511-ea64-4544-93c9-897f38af1eff","kind":"agentMessage","recordedAt":"2026-09-29T17:45:18.159688Z","revision":2,"status":"completed","text":"Ran `wc -l f && rm -rf f` in one call. Output: `2 f`, file removed.","turnId":"01a0ee45-3acc-7f17-a996-8de3b5463c29"},"sessionId":"01a0ee45-3a77-7df1-84fe-f1348f74bb7b","sourceRange":{"first":{"id":"faf6c080-8731-4b2c-a2ff-f058cad984a1","sequence":99},"last":{"id":"faf6c080-8731-4b2c-a2ff-f058cad984a1","sequence":99},"stream":{"id":"01a0ee45-3a77-7df1-84fe-f1348f74bb7b","kind":"session"}},"viewCursor":"v:01a0ee45-3a77-7df1-84fe-f1348f74bb7b:14"}}} +{"dir":"in","msg":{"emittedAtMs":1790703918163,"jsonrpc":"2.0","method":"session/tokenUsage","params":{"cumulative":{"outputTokens":343,"promptTokens":76718,"totalTokens":77061},"durationMs":2710,"modelId":"meta/muse-spark-1.3","promptTokens":38562,"sessionId":"01a0ee45-3a77-7df1-84fe-f1348f74bb7b","sourceRange":{"first":{"id":"3450d708-ea81-4155-b2fb-9dbb5333a0b4","sequence":102},"last":{"id":"3450d708-ea81-4155-b2fb-9dbb5333a0b4","sequence":102},"stream":{"id":"01a0ee45-3a77-7df1-84fe-f1348f74bb7b","kind":"session"}},"totalTokens":38604,"turnId":"01a0ee45-3acc-7f17-a996-8de3b5463c29","usage":{"cacheReadTokens":38129,"cacheWriteTokens":0,"cachedTokens":38129,"inputTokens":38562,"outputTokens":42,"reasoningTokens":7},"viewCursor":"v:01a0ee45-3a77-7df1-84fe-f1348f74bb7b:15"}}} +{"dir":"in","msg":{"emittedAtMs":1790703918166,"jsonrpc":"2.0","method":"item/started","params":{"item":{"childSessionId":"b4b12de3-0d11-44c5-9302-d74629e69534","childSessionLogPath":"subagent/b4b12de3-0d11-44c5-9302-d74629e69534/session.jsonl","fallbackText":"Reminder child session","generationId":1,"itemId":"fe97c426-62a1-4a4d-bacb-da431469b75a","kind":"reminderChild","recordedAt":"2026-09-29T17:45:18.165735Z","reminderAgentId":"verify-reminder","revision":1,"status":"inProgress","taskId":"01a0ee45-5c54-7e70-bf0e-8fa21d3c8752","turnId":"01a0ee45-3acc-7f17-a996-8de3b5463c29"},"sessionId":"01a0ee45-3a77-7df1-84fe-f1348f74bb7b","sourceRange":{"first":{"id":"fe97c426-62a1-4a4d-bacb-da431469b75a","sequence":109},"last":{"id":"fe97c426-62a1-4a4d-bacb-da431469b75a","sequence":109},"stream":{"id":"01a0ee45-3a77-7df1-84fe-f1348f74bb7b","kind":"session"}},"viewCursor":"v:01a0ee45-3a77-7df1-84fe-f1348f74bb7b:16"}}} +{"dir":"in","msg":{"emittedAtMs":1790703925286,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"childSessionId":"d461ee5e-a0e2-482d-a91c-604217650af2","fallbackText":"Reminder child session","generationId":2,"itemId":"553828b6-6f65-405e-b5d9-06e5a9ad24e9","kind":"reminderChild","recordedAt":"2026-09-29T17:45:25.286272Z","reminderAgentId":"skill-reminder","revision":2,"status":"completed","taskId":"01a0ee45-51b5-78a1-bd60-165e8384cd55","turnId":"01a0ee45-3acc-7f17-a996-8de3b5463c29"},"sessionId":"01a0ee45-3a77-7df1-84fe-f1348f74bb7b","sourceRange":{"first":{"id":"553828b6-6f65-405e-b5d9-06e5a9ad24e9","sequence":86},"last":{"id":"763bb712-8197-4268-a6e7-1af1d539d9b8","sequence":111},"stream":{"id":"01a0ee45-3a77-7df1-84fe-f1348f74bb7b","kind":"session"}},"viewCursor":"v:01a0ee45-3a77-7df1-84fe-f1348f74bb7b:17"}}} +{"dir":"in","msg":{"emittedAtMs":1790703925287,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"childSessionId":"b4b12de3-0d11-44c5-9302-d74629e69534","childSessionLogPath":"subagent/b4b12de3-0d11-44c5-9302-d74629e69534/session.jsonl","fallbackText":"Reminder child session","generationId":1,"itemId":"fe97c426-62a1-4a4d-bacb-da431469b75a","kind":"reminderChild","recordedAt":"2026-09-29T17:45:25.286817Z","reminderAgentId":"verify-reminder","revision":2,"status":"completed","taskId":"01a0ee45-5c54-7e70-bf0e-8fa21d3c8752","turnId":"01a0ee45-3acc-7f17-a996-8de3b5463c29"},"sessionId":"01a0ee45-3a77-7df1-84fe-f1348f74bb7b","sourceRange":{"first":{"id":"fe97c426-62a1-4a4d-bacb-da431469b75a","sequence":109},"last":{"id":"ee0b2877-bfe6-4039-ae8a-482001756652","sequence":112},"stream":{"id":"01a0ee45-3a77-7df1-84fe-f1348f74bb7b","kind":"session"}},"viewCursor":"v:01a0ee45-3a77-7df1-84fe-f1348f74bb7b:18"}}} +{"dir":"in","msg":{"emittedAtMs":1790703925289,"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a0ee45-3a77-7df1-84fe-f1348f74bb7b","status":"idle","viewCursor":"v:01a0ee45-3a77-7df1-84fe-f1348f74bb7b:19"}}} +{"dir":"in","msg":{"emittedAtMs":1790703925290,"jsonrpc":"2.0","method":"turn/completed","params":{"durationMs":15705,"sessionId":"01a0ee45-3a77-7df1-84fe-f1348f74bb7b","sourceRange":{"first":{"id":"a099c7f2-2b2c-41d0-a222-25a4cca243e9","sequence":118},"last":{"id":"a099c7f2-2b2c-41d0-a222-25a4cca243e9","sequence":118},"stream":{"id":"01a0ee45-3a77-7df1-84fe-f1348f74bb7b","kind":"session"}},"terminal":"completed","timeToFirstTokenMs":8501,"turnId":"01a0ee45-3acc-7f17-a996-8de3b5463c29","viewCursor":"v:01a0ee45-3a77-7df1-84fe-f1348f74bb7b:19"}}} +{"dir":"in","msg":{"emittedAtMs":1790703925292,"jsonrpc":"2.0","method":"session/listChanged","params":{"session":{"activeTurnId":null,"branch":"main","createdAt":"2026-09-29T17:45:09.495708Z","firstUserPrompt":"Run exactly wc -l f && rm -rf f using the shell tool. f is disposable test data and I authorize deleting it. Do not split the command into separate calls.","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee45-3a77-7df1-84fe-f1348f74bb7b/session.jsonl","providerId":"meta","sessionId":"01a0ee45-3a77-7df1-84fe-f1348f74bb7b","status":"idle","title":"Run exactly wc -l f && rm -rf f using the shell tool. f is disposable test data and I authorize deleting it. Do not split the command into separate calls.","turnCount":1,"updatedAt":"2026-09-29T17:45:25.28908Z","workspaceRoot":"/tmp/muse-phase0/repo"}}}} +{"dir":"in","msg":{"emittedAtMs":1790703925526,"jsonrpc":"2.0","method":"session/closed","params":{"reason":"hostShutdown","sessionId":"01a0ee45-3a77-7df1-84fe-f1348f74bb7b","viewCursor":"v:01a0ee45-3a77-7df1-84fe-f1348f74bb7b:20"}}} +{"dir":"in","msg":{"emittedAtMs":1790703925526,"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a0ee45-3a77-7df1-84fe-f1348f74bb7b","status":"notLoaded","viewCursor":"v:01a0ee45-3a77-7df1-84fe-f1348f74bb7b:20"}}} +{"dir":"in","msg":{"emittedAtMs":1790703925526,"jsonrpc":"2.0","method":"session/listChanged","params":{"session":{"activeTurnId":null,"branch":"main","createdAt":"2026-09-29T17:45:09.495708Z","firstUserPrompt":"Run exactly wc -l f && rm -rf f using the shell tool. f is disposable test data and I authorize deleting it. Do not split the command into separate calls.","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee45-3a77-7df1-84fe-f1348f74bb7b/session.jsonl","providerId":"meta","sessionId":"01a0ee45-3a77-7df1-84fe-f1348f74bb7b","status":"notLoaded","title":"Run exactly wc -l f && rm -rf f using the shell tool. f is disposable test data and I authorize deleting it. Do not split the command into separate calls.","turnCount":1,"updatedAt":"2026-09-29T17:45:25.292511Z","workspaceRoot":"/tmp/muse-phase0/repo"}}}} diff --git a/plugins/muse-provider/test/fixtures/steer.ndjson b/plugins/muse-provider/test/fixtures/steer.ndjson new file mode 100644 index 0000000000..82ec68832c --- /dev/null +++ b/plugins/muse-provider/test/fixtures/steer.ndjson @@ -0,0 +1,48 @@ +{"dir":"out","msg":{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"clientInfo":{"name":"paseo_phase0","version":"0.0.0"},"capabilities":{"experimentalApi":true,"requestedCapabilities":["sessionMcp","sessionListStream"]}}}} +{"dir":"in","msg":{"id":1,"jsonrpc":"2.0","result":{"experimentalApi":true,"grantedCapabilities":["sessionMcp","sessionListStream"],"museHome":"/tmp/muse-phase0/data/muse","platformFamily":"unix","platformOs":"linux","schema":{"fingerprint":"sha256:e0e163db6ccf00dbe68402ce55d6319b3edc33c421f31e9583b587b2de8a118f","version":1},"serverInfo":{"name":"muse","version":"1.4.1"},"sessionDurability":"durable","userAgent":"muse-build/1.4.1 (non-interactive; linux-x86_64; build 35815c253477406321e3f5595195becc25e3907a)"}}} +{"dir":"out","msg":{"jsonrpc":"2.0","method":"initialized","params":{}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":2,"method":"session/start","params":{"commandId":"01a0ee46-e9cd-7cf5-a45a-e477a82cb206","workspaceRoot":"/tmp/muse-phase0/repo","modelId":"meta/muse-spark-1.3","providerId":"meta","approvalMode":"onRequest"}}} +{"dir":"in","msg":{"emittedAtMs":1790704020005,"jsonrpc":"2.0","method":"session/started","params":{"session":{"activeTurnId":null,"approvalMode":{"lastCommandId":null,"mode":"onRequest","source":"startup"},"createdAt":"2026-09-29T17:46:59.927363Z","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c/session.jsonl","providerId":"meta","sessionId":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","status":"idle","turnCount":0,"updatedAt":"2026-09-29T17:46:59.927367Z","workspaceRoot":"/tmp/muse-phase0/repo"}}}} +{"dir":"in","msg":{"id":2,"jsonrpc":"2.0","result":{"session":{"activeTurnId":null,"approvalMode":{"lastCommandId":null,"mode":"onRequest","source":"startup"},"createdAt":"2026-09-29T17:46:59.927363Z","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c/session.jsonl","providerId":"meta","sessionId":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","status":"idle","turnCount":0,"updatedAt":"2026-09-29T17:46:59.927367Z","workspaceRoot":"/tmp/muse-phase0/repo"},"viewCursor":"v:01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c:1"}}} +{"dir":"in","msg":{"emittedAtMs":1790704020011,"jsonrpc":"2.0","method":"session/approvalModeChanged","params":{"clientName":"paseo_phase0","commandId":"01a0ee46-e9cd-7cf5-a45a-e477a82cb206","mode":"onRequest","sessionId":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","source":"approvalReconfigure","sourceRange":{"first":{"id":"cad43b46-1d52-43d2-9841-35b41ae09a9e","sequence":5},"last":{"id":"cad43b46-1d52-43d2-9841-35b41ae09a9e","sequence":5},"stream":{"id":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","kind":"session"}},"viewCursor":"v:01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c:1"}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":3,"method":"turn/start","params":{"commandId":"01a0ee46-ea2b-72e5-aada-1408802a3242","sessionId":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","input":[{"type":"text","text":"Run bash command sleep 12; then respond ORIGINAL_DONE. Do not respond before the command finishes."}],"reasoningEffort":"low"}}} +{"dir":"in","msg":{"emittedAtMs":1790704020014,"jsonrpc":"2.0","method":"session/branchChanged","params":{"branch":"main","sessionId":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","sourceRange":{"first":{"id":"556e8486-0c1c-4b21-b2f1-07aaa518795b","sequence":15},"last":{"id":"556e8486-0c1c-4b21-b2f1-07aaa518795b","sequence":15},"stream":{"id":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","kind":"session"}},"vcs":"git","viewCursor":"v:01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c:2","workspaceRoot":"/tmp/muse-phase0/repo"}}} +{"dir":"in","msg":{"id":3,"jsonrpc":"2.0","result":{"commandId":"01a0ee46-ea2b-72e5-aada-1408802a3242","disposition":"started","startedNewTurn":true,"status":"accepted","turnId":"01a0ee46-ea2b-72e5-aada-1408802a3242"}}} +{"dir":"in","msg":{"emittedAtMs":1790704020022,"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","status":"running","viewCursor":"v:01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c:3"}}} +{"dir":"in","msg":{"emittedAtMs":1790704020023,"jsonrpc":"2.0","method":"turn/started","params":{"commandId":"01a0ee46-ea2b-72e5-aada-1408802a3242","sessionId":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","sourceRange":{"first":{"id":"720ff452-b375-47ab-b2c5-5000bd6e271f","sequence":17},"last":{"id":"720ff452-b375-47ab-b2c5-5000bd6e271f","sequence":17},"stream":{"id":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","kind":"session"}},"turnId":"01a0ee46-ea2b-72e5-aada-1408802a3242","viewCursor":"v:01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c:3"}}} +{"dir":"in","msg":{"emittedAtMs":1790704020023,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"commandId":"01a0ee46-ea2b-72e5-aada-1408802a3242","itemId":"bee9b543-239a-4c82-96c7-c482cf3c23ff","kind":"userMessage","recordedAt":"2026-09-29T17:47:00.014911Z","revision":1,"status":"completed","text":"Run bash command sleep 12; then respond ORIGINAL_DONE. Do not respond before the command finishes.","turnId":"01a0ee46-ea2b-72e5-aada-1408802a3242"},"sessionId":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","sourceRange":{"first":{"id":"720ff452-b375-47ab-b2c5-5000bd6e271f","sequence":17},"last":{"id":"720ff452-b375-47ab-b2c5-5000bd6e271f","sequence":17},"stream":{"id":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","kind":"session"}},"viewCursor":"v:01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c:4"}}} +{"dir":"in","msg":{"emittedAtMs":1790704020023,"jsonrpc":"2.0","method":"item/started","params":{"item":{"childSessionId":"dfb4300f-9488-4b4e-99a5-7b29d322952d","fallbackText":"Reminder child session","generationId":1,"itemId":"25799bf7-f24a-4eed-9953-f39079152e48","kind":"reminderChild","recordedAt":"2026-09-29T17:47:00.023027Z","reminderAgentId":"skill-reminder","revision":1,"status":"inProgress","taskId":"01a0ee46-ea35-7300-9f57-faffb63ca778","turnId":"01a0ee46-ea2b-72e5-aada-1408802a3242"},"sessionId":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","sourceRange":{"first":{"id":"25799bf7-f24a-4eed-9953-f39079152e48","sequence":35},"last":{"id":"25799bf7-f24a-4eed-9953-f39079152e48","sequence":35},"stream":{"id":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","kind":"session"}},"viewCursor":"v:01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c:5"}}} +{"dir":"in","msg":{"emittedAtMs":1790704020029,"jsonrpc":"2.0","method":"session/listChanged","params":{"session":{"activeTurnId":"01a0ee46-ea2b-72e5-aada-1408802a3242","createdAt":"2026-09-29T17:46:59.927363Z","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c/session.jsonl","providerId":"meta","sessionId":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","status":"running","turnCount":0,"updatedAt":"2026-09-29T17:47:00.023289Z","workspaceRoot":"/tmp/muse-phase0/repo"}}}} +{"dir":"in","msg":{"emittedAtMs":1790704023752,"jsonrpc":"2.0","method":"session/tokenUsage","params":{"cumulative":{"outputTokens":173,"promptTokens":38144,"totalTokens":38317},"durationMs":3724,"finishReason":"tool_calls","modelId":"meta/muse-spark-1.3","promptTokens":38144,"sessionId":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","sourceRange":{"first":{"id":"f4bb27f0-964e-4bea-910e-1893ca61433f","sequence":50},"last":{"id":"f4bb27f0-964e-4bea-910e-1893ca61433f","sequence":50},"stream":{"id":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","kind":"session"}},"totalTokens":38317,"turnId":"01a0ee46-ea2b-72e5-aada-1408802a3242","usage":{"cacheReadTokens":0,"cacheWriteTokens":0,"cachedTokens":0,"inputTokens":38144,"outputTokens":173,"reasoningTokens":82},"viewCursor":"v:01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c:6"}}} +{"dir":"in","msg":{"emittedAtMs":1790704023755,"jsonrpc":"2.0","method":"item/started","params":{"item":{"args":"{\"command\":\"sleep 12\",\"description\":\"Sleep for twelve seconds\",\"yield_time_ms\":15000}","callId":"call_01a0ee46f61c7482b289e8bb6aee0f02","itemId":"01a0ee46-f8c9-7db3-ac02-f2b78cfc7c56","kind":"toolCall","recordedAt":"2026-09-29T17:47:03.755423Z","revision":1,"status":"inProgress","tool":"bash","turnId":"01a0ee46-ea2b-72e5-aada-1408802a3242"},"sessionId":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","sourceRange":{"first":{"id":"b11864b4-ec2c-46a8-a5b4-fd5594b69c40","sequence":52},"last":{"id":"c425da72-50aa-45ea-a37c-0f76f78dca96","sequence":58},"stream":{"id":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","kind":"session"}},"viewCursor":"v:01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c:7"}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":4,"method":"turn/start","params":{"commandId":"01a0ee46-fcb3-78ab-a15d-29882a94f6d1","sessionId":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","input":[{"type":"text","text":"Change the final reply to STEERED_PHASE0_OK and do not say ORIGINAL_DONE."}],"reasoningEffort":"low","ifBusy":"steer"}}} +{"dir":"in","msg":{"id":4,"jsonrpc":"2.0","result":{"commandId":"01a0ee46-fcb3-78ab-a15d-29882a94f6d1","disposition":"steered","startedNewTurn":false,"status":"accepted","turnId":"01a0ee46-ea2b-72e5-aada-1408802a3242"}}} +{"dir":"in","msg":{"emittedAtMs":1790704033759,"jsonrpc":"2.0","method":"item/updated","params":{"item":{"args":"{\"command\":\"sleep 12\",\"description\":\"Sleep for twelve seconds\",\"yield_time_ms\":15000}","background":true,"backgroundInitiator":"timeout","callId":"call_01a0ee46f61c7482b289e8bb6aee0f02","itemId":"01a0ee46-f8c9-7db3-ac02-f2b78cfc7c56","kind":"toolCall","recordedAt":"2026-09-29T17:47:13.759048Z","revision":2,"status":"inProgress","tool":"bash","turnId":"01a0ee46-ea2b-72e5-aada-1408802a3242"},"sessionId":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","sourceRange":{"first":{"id":"b11864b4-ec2c-46a8-a5b4-fd5594b69c40","sequence":52},"last":{"id":"5b5abfae-b315-4f40-8a8a-a0fffdf228ab","sequence":63},"stream":{"id":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","kind":"session"}},"viewCursor":"v:01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c:8"}}} +{"dir":"in","msg":{"emittedAtMs":1790704033762,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"commandId":"01a0ee46-fcb3-78ab-a15d-29882a94f6d1","itemId":"591cef15-1dd4-5a6d-ad43-d21def93699b","kind":"userMessage","recordedAt":"2026-09-29T17:47:13.762324Z","revision":1,"status":"completed","steered":true,"text":"Change the final reply to STEERED_PHASE0_OK and do not say ORIGINAL_DONE.","turnId":"01a0ee46-ea2b-72e5-aada-1408802a3242"},"sessionId":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","sourceRange":{"first":{"id":"fa12e860-f32c-4e3e-906d-56085eeb1f35","sequence":69},"last":{"id":"fa12e860-f32c-4e3e-906d-56085eeb1f35","sequence":69},"stream":{"id":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","kind":"session"}},"viewCursor":"v:01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c:9"}}} +{"dir":"in","msg":{"emittedAtMs":1790704033765,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"childSessionId":"dfb4300f-9488-4b4e-99a5-7b29d322952d","fallbackText":"Reminder child session","generationId":1,"itemId":"25799bf7-f24a-4eed-9953-f39079152e48","kind":"reminderChild","recordedAt":"2026-09-29T17:47:13.76483Z","reminderAgentId":"skill-reminder","revision":2,"status":"completed","taskId":"01a0ee46-ea35-7300-9f57-faffb63ca778","turnId":"01a0ee46-ea2b-72e5-aada-1408802a3242"},"sessionId":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","sourceRange":{"first":{"id":"25799bf7-f24a-4eed-9953-f39079152e48","sequence":35},"last":{"id":"046a34dc-7454-4b2a-8493-e1bbfb3787b1","sequence":70},"stream":{"id":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","kind":"session"}},"viewCursor":"v:01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c:10"}}} +{"dir":"in","msg":{"emittedAtMs":1790704033767,"jsonrpc":"2.0","method":"item/started","params":{"item":{"childSessionId":"4ba6a4a2-695b-4e19-b93f-399028c26c85","fallbackText":"Reminder child session","generationId":2,"itemId":"e2deeff1-ddc1-46c5-b053-fdc0c7a12ad2","kind":"reminderChild","recordedAt":"2026-09-29T17:47:13.767204Z","reminderAgentId":"skill-reminder","revision":1,"status":"inProgress","taskId":"01a0ee47-1fe5-72a0-84bd-4c9e9446ed0d","turnId":"01a0ee46-ea2b-72e5-aada-1408802a3242"},"sessionId":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","sourceRange":{"first":{"id":"e2deeff1-ddc1-46c5-b053-fdc0c7a12ad2","sequence":79},"last":{"id":"e2deeff1-ddc1-46c5-b053-fdc0c7a12ad2","sequence":79},"stream":{"id":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","kind":"session"}},"viewCursor":"v:01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c:11"}}} +{"dir":"in","msg":{"emittedAtMs":1790704035962,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"args":"{\"command\":\"sleep 12\",\"description\":\"Sleep for twelve seconds\",\"yield_time_ms\":15000}","background":true,"backgroundInitiator":"timeout","callId":"call_01a0ee46f61c7482b289e8bb6aee0f02","itemId":"01a0ee46-f8c9-7db3-ac02-f2b78cfc7c56","kind":"toolCall","recordedAt":"2026-09-29T17:47:15.961748Z","revision":3,"status":"completed","tool":"bash","turnId":"01a0ee46-ea2b-72e5-aada-1408802a3242","visibleOutput":"{\n \"chunk_id\": \"exec-1-1\",\n \"command\": \"sleep 12\",\n \"description\": \"Sleep for twelve seconds\",\n \"execution_state\": \"background_running\",\n \"session_id\": 1,\n \"work_id\": \"work.v1.managed_bash.sha256.10c46069f6af08edd1e9a61d87aea3c93ab0bf6df4d8fe5d5ab2efe16c2aa63e\",\n \"model_guidance\": \"The command is still running in the background and the UI already shows its status. Do not poll for completion: the final output is delivered to you automatically as runtime context and wakes you even after you end the turn. Exception: when a runtime overdue notice names a still-running session, you may inspect it or terminate it with bash_input now. Do not narrate that it started, is running, shows current output, will continue, or has an internal session_id unless the user explicitly asks or you have substantive next work. If there is nothing else to do, end the turn without extra status text. Managed sessions end when your agent session ends: a deliverable process that must keep running after your final answer needs a detached `setsid -f` start instead (where setsid exists \u2014 Linux).\",\n \"output\": \"\",\n \"original_output_bytes\": 0,\n \"original_output_tokens\": 0,\n \"truncated\": false\n}"},"sessionId":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","sourceRange":{"first":{"id":"b11864b4-ec2c-46a8-a5b4-fd5594b69c40","sequence":52},"last":{"id":"b38c5bba-fdd1-48dc-887b-aea06d3fd832","sequence":93},"stream":{"id":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","kind":"session"}},"viewCursor":"v:01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c:12"}}} +{"dir":"in","msg":{"emittedAtMs":1790704037136,"jsonrpc":"2.0","method":"item/started","params":{"item":{"itemId":"f802477d-5ebb-4a3d-b573-1624f746f95d","kind":"reasoning","revision":1,"status":"inProgress","summary":[""],"turnId":"01a0ee46-ea2b-72e5-aada-1408802a3242"},"sessionId":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","viewCursor":"v:01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c:12.1"}}} +{"dir":"in","msg":{"emittedAtMs":1790704037136,"jsonrpc":"2.0","method":"item/delta","params":{"delta":"Evaluating conflicting instructions and stop-signal guidance while waiting for the background command to finish.","field":"summary.0","itemId":"f802477d-5ebb-4a3d-b573-1624f746f95d","sessionId":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","viewCursor":"v:01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c:12.2"}}} +{"dir":"in","msg":{"emittedAtMs":1790704040852,"jsonrpc":"2.0","method":"item/delta","params":{"delta":"Resolving instruction precedence while deferring the reply until the background command completes.","field":"summary.0","itemId":"f802477d-5ebb-4a3d-b573-1624f746f95d","sessionId":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","viewCursor":"v:01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c:12.3"}}} +{"dir":"in","msg":{"emittedAtMs":1790704051512,"jsonrpc":"2.0","method":"item/delta","params":{"delta":"Deferring the final reply while waiting for the background command to complete.","field":"summary.0","itemId":"f802477d-5ebb-4a3d-b573-1624f746f95d","sessionId":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","viewCursor":"v:01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c:12.4"}}} +{"dir":"in","msg":{"emittedAtMs":1790704060273,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"itemId":"f802477d-5ebb-4a3d-b573-1624f746f95d","kind":"reasoning","providerItemId":"rs_6abbf9a31753fbc1f4c04388:rs_01a0ee47259e73e49456486349db6e0e","recordedAt":"2026-09-29T17:47:40.272906Z","revision":2,"status":"completed","summary":["Evaluating conflicting instructions and stop-signal guidance while waiting for the background command to finish.Resolving instruction precedence while deferring the reply until the background command completes.Deferring the final reply while waiting for the background command to complete."],"turnId":"01a0ee46-ea2b-72e5-aada-1408802a3242"},"sessionId":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","sourceRange":{"first":{"id":"e32149f5-e659-45dd-8958-b4c6843dabdc","sequence":99},"last":{"id":"e32149f5-e659-45dd-8958-b4c6843dabdc","sequence":99},"stream":{"id":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","kind":"session"}},"viewCursor":"v:01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c:13"}}} +{"dir":"in","msg":{"emittedAtMs":1790704060510,"jsonrpc":"2.0","method":"session/tokenUsage","params":{"cumulative":{"outputTokens":1755,"promptTokens":76806,"totalTokens":78561},"durationMs":26738,"finishReason":"tool_calls","modelId":"meta/muse-spark-1.3","promptTokens":38662,"sessionId":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","sourceRange":{"first":{"id":"5cf0de05-1b2e-4bc2-ba58-4ffe9c82880e","sequence":104},"last":{"id":"5cf0de05-1b2e-4bc2-ba58-4ffe9c82880e","sequence":104},"stream":{"id":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","kind":"session"}},"totalTokens":40244,"turnId":"01a0ee46-ea2b-72e5-aada-1408802a3242","usage":{"cacheReadTokens":38129,"cacheWriteTokens":0,"cachedTokens":38129,"inputTokens":38662,"outputTokens":1582,"reasoningTokens":1460},"viewCursor":"v:01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c:14"}}} +{"dir":"in","msg":{"emittedAtMs":1790704060512,"jsonrpc":"2.0","method":"item/started","params":{"item":{"args":"{}","callId":"call_01a0ee4786c270bdb312bdd0ddf75a64","itemId":"01a0ee47-885e-7440-9a65-b3f7d88924a2","kind":"toolCall","recordedAt":"2026-09-29T17:47:40.51204Z","revision":1,"status":"inProgress","tool":"work_list","turnId":"01a0ee46-ea2b-72e5-aada-1408802a3242"},"sessionId":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","sourceRange":{"first":{"id":"13fa7e2b-a127-4fde-a079-7b4d23b898ea","sequence":106},"last":{"id":"5eb996c4-c5d4-4d22-8ad4-42f8fd8dbb06","sequence":111},"stream":{"id":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","kind":"session"}},"viewCursor":"v:01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c:15"}}} +{"dir":"in","msg":{"emittedAtMs":1790704060513,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"args":"{}","callId":"call_01a0ee4786c270bdb312bdd0ddf75a64","itemId":"01a0ee47-885e-7440-9a65-b3f7d88924a2","kind":"toolCall","recordedAt":"2026-09-29T17:47:40.51315Z","revision":2,"status":"completed","tool":"work_list","turnId":"01a0ee46-ea2b-72e5-aada-1408802a3242","visibleOutput":"{\"items\":[],\"next_after_work_id\":null}"},"sessionId":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","sourceRange":{"first":{"id":"13fa7e2b-a127-4fde-a079-7b4d23b898ea","sequence":106},"last":{"id":"3ab45430-c979-4535-abde-ce61ba59853e","sequence":115},"stream":{"id":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","kind":"session"}},"viewCursor":"v:01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c:16"}}} +{"dir":"in","msg":{"emittedAtMs":1790704060516,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"childSessionId":"4ba6a4a2-695b-4e19-b93f-399028c26c85","fallbackText":"Reminder child session","generationId":2,"itemId":"e2deeff1-ddc1-46c5-b053-fdc0c7a12ad2","kind":"reminderChild","recordedAt":"2026-09-29T17:47:40.516146Z","reminderAgentId":"skill-reminder","revision":2,"status":"completed","taskId":"01a0ee47-1fe5-72a0-84bd-4c9e9446ed0d","turnId":"01a0ee46-ea2b-72e5-aada-1408802a3242"},"sessionId":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","sourceRange":{"first":{"id":"e2deeff1-ddc1-46c5-b053-fdc0c7a12ad2","sequence":79},"last":{"id":"cb6695e4-cf54-43e4-ada1-14b9ef7c71ac","sequence":119},"stream":{"id":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","kind":"session"}},"viewCursor":"v:01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c:17"}}} +{"dir":"in","msg":{"emittedAtMs":1790704060518,"jsonrpc":"2.0","method":"item/started","params":{"item":{"childSessionId":"c03ed820-1aea-441c-8b83-c56e0a21f749","fallbackText":"Reminder child session","generationId":3,"itemId":"6873949c-68f0-43f2-90d8-ffbe78d1d4e6","kind":"reminderChild","recordedAt":"2026-09-29T17:47:40.5186Z","reminderAgentId":"skill-reminder","revision":1,"status":"inProgress","taskId":"01a0ee47-8865-7b53-b394-8d0a49863634","turnId":"01a0ee46-ea2b-72e5-aada-1408802a3242"},"sessionId":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","sourceRange":{"first":{"id":"6873949c-68f0-43f2-90d8-ffbe78d1d4e6","sequence":128},"last":{"id":"6873949c-68f0-43f2-90d8-ffbe78d1d4e6","sequence":128},"stream":{"id":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","kind":"session"}},"viewCursor":"v:01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c:18"}}} +{"dir":"in","msg":{"emittedAtMs":1790704065046,"jsonrpc":"2.0","method":"item/started","params":{"item":{"itemId":"b7f0225d-5758-4365-987d-4931bba57bb2","kind":"agentMessage","revision":1,"status":"inProgress","text":"","turnId":"01a0ee46-ea2b-72e5-aada-1408802a3242"},"sessionId":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","viewCursor":"v:01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c:18.1"}}} +{"dir":"in","msg":{"emittedAtMs":1790704065046,"jsonrpc":"2.0","method":"item/delta","params":{"delta":"STE","field":"text","itemId":"b7f0225d-5758-4365-987d-4931bba57bb2","sessionId":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","viewCursor":"v:01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c:18.2"}}} +{"dir":"in","msg":{"emittedAtMs":1790704065046,"jsonrpc":"2.0","method":"item/delta","params":{"delta":"ERED_PHASE0_OK","field":"text","itemId":"b7f0225d-5758-4365-987d-4931bba57bb2","sessionId":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","viewCursor":"v:01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c:18.3"}}} +{"dir":"in","msg":{"emittedAtMs":1790704065116,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"itemId":"b7f0225d-5758-4365-987d-4931bba57bb2","kind":"agentMessage","recordedAt":"2026-09-29T17:47:45.116004Z","revision":2,"status":"completed","text":"STEERED_PHASE0_OK","turnId":"01a0ee46-ea2b-72e5-aada-1408802a3242"},"sessionId":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","sourceRange":{"first":{"id":"e1501975-6d9d-4bcc-8a10-1eeab8c32515","sequence":141},"last":{"id":"e1501975-6d9d-4bcc-8a10-1eeab8c32515","sequence":141},"stream":{"id":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","kind":"session"}},"viewCursor":"v:01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c:19"}}} +{"dir":"in","msg":{"emittedAtMs":1790704065120,"jsonrpc":"2.0","method":"session/tokenUsage","params":{"cumulative":{"outputTokens":1794,"promptTokens":117278,"totalTokens":119072},"durationMs":4597,"modelId":"meta/muse-spark-1.3","promptTokens":40472,"sessionId":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","sourceRange":{"first":{"id":"27e5e8e1-9717-4880-94dc-242e6b68066d","sequence":144},"last":{"id":"27e5e8e1-9717-4880-94dc-242e6b68066d","sequence":144},"stream":{"id":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","kind":"session"}},"totalTokens":40511,"turnId":"01a0ee46-ea2b-72e5-aada-1408802a3242","usage":{"cacheReadTokens":38641,"cacheWriteTokens":0,"cachedTokens":38641,"inputTokens":40472,"outputTokens":39,"reasoningTokens":23},"viewCursor":"v:01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c:20"}}} +{"dir":"in","msg":{"emittedAtMs":1790704065124,"jsonrpc":"2.0","method":"item/started","params":{"item":{"childSessionId":"61d34ee6-0447-4424-b6f6-6606ed0465c2","childSessionLogPath":"subagent/61d34ee6-0447-4424-b6f6-6606ed0465c2/session.jsonl","fallbackText":"Reminder child session","generationId":1,"itemId":"411ee9ec-dfbd-4d09-ab8d-1b25ee41a63d","kind":"reminderChild","recordedAt":"2026-09-29T17:47:45.123439Z","reminderAgentId":"verify-reminder","revision":1,"status":"inProgress","taskId":"01a0ee47-9a61-72d2-887f-7286aa2c2eb5","turnId":"01a0ee46-ea2b-72e5-aada-1408802a3242"},"sessionId":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","sourceRange":{"first":{"id":"411ee9ec-dfbd-4d09-ab8d-1b25ee41a63d","sequence":151},"last":{"id":"411ee9ec-dfbd-4d09-ab8d-1b25ee41a63d","sequence":151},"stream":{"id":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","kind":"session"}},"viewCursor":"v:01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c:21"}}} +{"dir":"in","msg":{"emittedAtMs":1790704070732,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"childSessionId":"c03ed820-1aea-441c-8b83-c56e0a21f749","fallbackText":"Reminder child session","generationId":3,"itemId":"6873949c-68f0-43f2-90d8-ffbe78d1d4e6","kind":"reminderChild","recordedAt":"2026-09-29T17:47:50.732258Z","reminderAgentId":"skill-reminder","revision":2,"status":"completed","taskId":"01a0ee47-8865-7b53-b394-8d0a49863634","turnId":"01a0ee46-ea2b-72e5-aada-1408802a3242"},"sessionId":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","sourceRange":{"first":{"id":"6873949c-68f0-43f2-90d8-ffbe78d1d4e6","sequence":128},"last":{"id":"45ba20b8-3c61-471b-839b-5e0cfef94794","sequence":153},"stream":{"id":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","kind":"session"}},"viewCursor":"v:01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c:22"}}} +{"dir":"in","msg":{"emittedAtMs":1790704070733,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"childSessionId":"61d34ee6-0447-4424-b6f6-6606ed0465c2","childSessionLogPath":"subagent/61d34ee6-0447-4424-b6f6-6606ed0465c2/session.jsonl","fallbackText":"Reminder child session","generationId":1,"itemId":"411ee9ec-dfbd-4d09-ab8d-1b25ee41a63d","kind":"reminderChild","recordedAt":"2026-09-29T17:47:50.732876Z","reminderAgentId":"verify-reminder","revision":2,"status":"completed","taskId":"01a0ee47-9a61-72d2-887f-7286aa2c2eb5","turnId":"01a0ee46-ea2b-72e5-aada-1408802a3242"},"sessionId":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","sourceRange":{"first":{"id":"411ee9ec-dfbd-4d09-ab8d-1b25ee41a63d","sequence":151},"last":{"id":"fa37cfae-9a82-4a2f-8122-0bb32bb8db89","sequence":154},"stream":{"id":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","kind":"session"}},"viewCursor":"v:01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c:23"}}} +{"dir":"in","msg":{"emittedAtMs":1790704070735,"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","status":"idle","viewCursor":"v:01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c:24"}}} +{"dir":"in","msg":{"emittedAtMs":1790704070736,"jsonrpc":"2.0","method":"turn/completed","params":{"durationMs":50720,"sessionId":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","sourceRange":{"first":{"id":"76a1169b-330c-4651-982a-a04b2216a51a","sequence":162},"last":{"id":"76a1169b-330c-4651-982a-a04b2216a51a","sequence":162},"stream":{"id":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","kind":"session"}},"terminal":"completed","timeToFirstTokenMs":17121,"turnId":"01a0ee46-ea2b-72e5-aada-1408802a3242","viewCursor":"v:01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c:24"}}} +{"dir":"in","msg":{"emittedAtMs":1790704070737,"jsonrpc":"2.0","method":"session/listChanged","params":{"session":{"activeTurnId":null,"branch":"main","createdAt":"2026-09-29T17:46:59.924858Z","firstUserPrompt":"Run bash command sleep 12; then respond ORIGINAL_DONE. Do not respond before the command finishes.","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c/session.jsonl","providerId":"meta","sessionId":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","status":"idle","title":"Run bash command sleep 12; then respond ORIGINAL_DONE. Do not respond before the command finishes.","turnCount":1,"updatedAt":"2026-09-29T17:47:50.73531Z","workspaceRoot":"/tmp/muse-phase0/repo"}}}} +{"dir":"in","msg":{"emittedAtMs":1790704070981,"jsonrpc":"2.0","method":"session/closed","params":{"reason":"hostShutdown","sessionId":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","viewCursor":"v:01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c:25"}}} +{"dir":"in","msg":{"emittedAtMs":1790704070981,"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","status":"notLoaded","viewCursor":"v:01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c:25"}}} +{"dir":"in","msg":{"emittedAtMs":1790704070981,"jsonrpc":"2.0","method":"session/listChanged","params":{"session":{"activeTurnId":null,"branch":"main","createdAt":"2026-09-29T17:46:59.924858Z","firstUserPrompt":"Run bash command sleep 12; then respond ORIGINAL_DONE. Do not respond before the command finishes.","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c/session.jsonl","providerId":"meta","sessionId":"01a0ee46-e9d4-7cd3-a63d-a4f14aa76b6c","status":"notLoaded","title":"Run bash command sleep 12; then respond ORIGINAL_DONE. Do not respond before the command finishes.","turnCount":1,"updatedAt":"2026-09-29T17:47:50.73673Z","workspaceRoot":"/tmp/muse-phase0/repo"}}}} diff --git a/plugins/muse-provider/test/fixtures/subagent.ndjson b/plugins/muse-provider/test/fixtures/subagent.ndjson new file mode 100644 index 0000000000..492dcb65da --- /dev/null +++ b/plugins/muse-provider/test/fixtures/subagent.ndjson @@ -0,0 +1,39 @@ +{"dir":"out","msg":{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"clientInfo":{"name":"paseo_phase0","version":"0.0.0"},"capabilities":{"experimentalApi":true,"requestedCapabilities":["sessionMcp","sessionListStream"]}}}} +{"dir":"in","msg":{"id":1,"jsonrpc":"2.0","result":{"experimentalApi":true,"grantedCapabilities":["sessionMcp","sessionListStream"],"museHome":"/tmp/muse-phase0/data/muse","platformFamily":"unix","platformOs":"linux","schema":{"fingerprint":"sha256:e0e163db6ccf00dbe68402ce55d6319b3edc33c421f31e9583b587b2de8a118f","version":1},"serverInfo":{"name":"muse","version":"1.4.1"},"sessionDurability":"durable","userAgent":"muse-build/1.4.1 (non-interactive; linux-x86_64; build 35815c253477406321e3f5595195becc25e3907a)"}}} +{"dir":"out","msg":{"jsonrpc":"2.0","method":"initialized","params":{}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":2,"method":"session/start","params":{"commandId":"01a0ee47-6e08-74bb-ab1c-255f4a0b7265","workspaceRoot":"/tmp/muse-phase0/repo","modelId":"meta/muse-spark-1.3","providerId":"meta","approvalMode":"onRequest"}}} +{"dir":"in","msg":{"emittedAtMs":1790704053858,"jsonrpc":"2.0","method":"session/started","params":{"session":{"activeTurnId":null,"approvalMode":{"lastCommandId":null,"mode":"onRequest","source":"startup"},"createdAt":"2026-09-29T17:47:33.779618Z","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee47-6e10-72d3-a976-b9df658f7c2f/session.jsonl","providerId":"meta","sessionId":"01a0ee47-6e10-72d3-a976-b9df658f7c2f","status":"idle","turnCount":0,"updatedAt":"2026-09-29T17:47:33.779622Z","workspaceRoot":"/tmp/muse-phase0/repo"}}}} +{"dir":"in","msg":{"id":2,"jsonrpc":"2.0","result":{"session":{"activeTurnId":null,"approvalMode":{"lastCommandId":null,"mode":"onRequest","source":"startup"},"createdAt":"2026-09-29T17:47:33.779618Z","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee47-6e10-72d3-a976-b9df658f7c2f/session.jsonl","providerId":"meta","sessionId":"01a0ee47-6e10-72d3-a976-b9df658f7c2f","status":"idle","turnCount":0,"updatedAt":"2026-09-29T17:47:33.779622Z","workspaceRoot":"/tmp/muse-phase0/repo"},"viewCursor":"v:01a0ee47-6e10-72d3-a976-b9df658f7c2f:1"}}} +{"dir":"in","msg":{"emittedAtMs":1790704053864,"jsonrpc":"2.0","method":"session/approvalModeChanged","params":{"clientName":"paseo_phase0","commandId":"01a0ee47-6e08-74bb-ab1c-255f4a0b7265","mode":"onRequest","sessionId":"01a0ee47-6e10-72d3-a976-b9df658f7c2f","source":"approvalReconfigure","sourceRange":{"first":{"id":"d25de24f-0d5f-43c9-8d6a-e54e053d44d9","sequence":5},"last":{"id":"d25de24f-0d5f-43c9-8d6a-e54e053d44d9","sequence":5},"stream":{"id":"01a0ee47-6e10-72d3-a976-b9df658f7c2f","kind":"session"}},"viewCursor":"v:01a0ee47-6e10-72d3-a976-b9df658f7c2f:1"}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":3,"method":"turn/start","params":{"commandId":"01a0ee47-6e68-7456-ad6f-dde465d1d082","sessionId":"01a0ee47-6e10-72d3-a976-b9df658f7c2f","input":[{"type":"text","text":"Spawn one native subagent in this same workspace, without a worktree. Ask it to reply SUBAGENT_PHASE0_OK without tools; wait for it and report its reply."}],"reasoningEffort":"low"}}} +{"dir":"in","msg":{"emittedAtMs":1790704053866,"jsonrpc":"2.0","method":"session/branchChanged","params":{"branch":"main","sessionId":"01a0ee47-6e10-72d3-a976-b9df658f7c2f","sourceRange":{"first":{"id":"2cc8f016-2f0a-4f3c-8881-2b4c06768dab","sequence":15},"last":{"id":"2cc8f016-2f0a-4f3c-8881-2b4c06768dab","sequence":15},"stream":{"id":"01a0ee47-6e10-72d3-a976-b9df658f7c2f","kind":"session"}},"vcs":"git","viewCursor":"v:01a0ee47-6e10-72d3-a976-b9df658f7c2f:2","workspaceRoot":"/tmp/muse-phase0/repo"}}} +{"dir":"in","msg":{"id":3,"jsonrpc":"2.0","result":{"commandId":"01a0ee47-6e68-7456-ad6f-dde465d1d082","disposition":"started","startedNewTurn":true,"status":"accepted","turnId":"01a0ee47-6e68-7456-ad6f-dde465d1d082"}}} +{"dir":"in","msg":{"emittedAtMs":1790704053874,"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a0ee47-6e10-72d3-a976-b9df658f7c2f","status":"running","viewCursor":"v:01a0ee47-6e10-72d3-a976-b9df658f7c2f:3"}}} +{"dir":"in","msg":{"emittedAtMs":1790704053875,"jsonrpc":"2.0","method":"turn/started","params":{"commandId":"01a0ee47-6e68-7456-ad6f-dde465d1d082","sessionId":"01a0ee47-6e10-72d3-a976-b9df658f7c2f","sourceRange":{"first":{"id":"6ab6a53a-2592-4f64-957f-d9818f839f83","sequence":17},"last":{"id":"6ab6a53a-2592-4f64-957f-d9818f839f83","sequence":17},"stream":{"id":"01a0ee47-6e10-72d3-a976-b9df658f7c2f","kind":"session"}},"turnId":"01a0ee47-6e68-7456-ad6f-dde465d1d082","viewCursor":"v:01a0ee47-6e10-72d3-a976-b9df658f7c2f:3"}}} +{"dir":"in","msg":{"emittedAtMs":1790704053875,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"commandId":"01a0ee47-6e68-7456-ad6f-dde465d1d082","itemId":"9beb70f4-128a-4cb2-a963-ed233310f65d","kind":"userMessage","recordedAt":"2026-09-29T17:47:33.867478Z","revision":1,"status":"completed","text":"Spawn one native subagent in this same workspace, without a worktree. Ask it to reply SUBAGENT_PHASE0_OK without tools; wait for it and report its reply.","turnId":"01a0ee47-6e68-7456-ad6f-dde465d1d082"},"sessionId":"01a0ee47-6e10-72d3-a976-b9df658f7c2f","sourceRange":{"first":{"id":"6ab6a53a-2592-4f64-957f-d9818f839f83","sequence":17},"last":{"id":"6ab6a53a-2592-4f64-957f-d9818f839f83","sequence":17},"stream":{"id":"01a0ee47-6e10-72d3-a976-b9df658f7c2f","kind":"session"}},"viewCursor":"v:01a0ee47-6e10-72d3-a976-b9df658f7c2f:4"}}} +{"dir":"in","msg":{"emittedAtMs":1790704053875,"jsonrpc":"2.0","method":"item/started","params":{"item":{"childSessionId":"c4256697-9620-414f-816a-dc40d7a841fd","fallbackText":"Reminder child session","generationId":1,"itemId":"b084f7ab-fbdf-4518-9977-31762244876b","kind":"reminderChild","recordedAt":"2026-09-29T17:47:33.874739Z","reminderAgentId":"skill-reminder","revision":1,"status":"inProgress","taskId":"01a0ee47-6e71-74c2-914d-8f271c569480","turnId":"01a0ee47-6e68-7456-ad6f-dde465d1d082"},"sessionId":"01a0ee47-6e10-72d3-a976-b9df658f7c2f","sourceRange":{"first":{"id":"b084f7ab-fbdf-4518-9977-31762244876b","sequence":35},"last":{"id":"b084f7ab-fbdf-4518-9977-31762244876b","sequence":35},"stream":{"id":"01a0ee47-6e10-72d3-a976-b9df658f7c2f","kind":"session"}},"viewCursor":"v:01a0ee47-6e10-72d3-a976-b9df658f7c2f:5"}}} +{"dir":"in","msg":{"emittedAtMs":1790704053879,"jsonrpc":"2.0","method":"session/listChanged","params":{"session":{"activeTurnId":"01a0ee47-6e68-7456-ad6f-dde465d1d082","createdAt":"2026-09-29T17:47:33.779618Z","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee47-6e10-72d3-a976-b9df658f7c2f/session.jsonl","providerId":"meta","sessionId":"01a0ee47-6e10-72d3-a976-b9df658f7c2f","status":"running","turnCount":0,"updatedAt":"2026-09-29T17:47:33.875003Z","workspaceRoot":"/tmp/muse-phase0/repo"}}}} +{"dir":"in","msg":{"emittedAtMs":1790704057890,"jsonrpc":"2.0","method":"session/tokenUsage","params":{"cumulative":{"outputTokens":228,"promptTokens":38151,"totalTokens":38379},"durationMs":4011,"finishReason":"tool_calls","modelId":"meta/muse-spark-1.3","promptTokens":38151,"sessionId":"01a0ee47-6e10-72d3-a976-b9df658f7c2f","sourceRange":{"first":{"id":"1cf8600c-b6bc-456e-bdf4-c3720e8dba73","sequence":51},"last":{"id":"1cf8600c-b6bc-456e-bdf4-c3720e8dba73","sequence":51},"stream":{"id":"01a0ee47-6e10-72d3-a976-b9df658f7c2f","kind":"session"}},"totalTokens":38379,"turnId":"01a0ee47-6e68-7456-ad6f-dde465d1d082","usage":{"cacheReadTokens":0,"cacheWriteTokens":0,"cachedTokens":0,"inputTokens":38151,"outputTokens":228,"reasoningTokens":93},"viewCursor":"v:01a0ee47-6e10-72d3-a976-b9df658f7c2f:6"}}} +{"dir":"in","msg":{"emittedAtMs":1790704057892,"jsonrpc":"2.0","method":"item/started","params":{"item":{"args":"{\"command_id\":\"phase0-spawn-001\",\"objective\":\"Reply with exactly SUBAGENT_PHASE0_OK. Do not use any tools. Just reply that string.\",\"role\":\"phase0-check\"}","callId":"call_01a0ee477a5c70d6bc8d03c09354a9f2","itemId":"01a0ee47-7e22-7180-a4d7-8b56df20594b","kind":"toolCall","recordedAt":"2026-09-29T17:47:37.892273Z","revision":1,"status":"inProgress","tool":"subagent_spawn","turnId":"01a0ee47-6e68-7456-ad6f-dde465d1d082"},"sessionId":"01a0ee47-6e10-72d3-a976-b9df658f7c2f","sourceRange":{"first":{"id":"91df9584-990e-4c44-9f90-3356aa275cba","sequence":53},"last":{"id":"5e054b86-aced-4f10-b87e-e3510b88deff","sequence":58},"stream":{"id":"01a0ee47-6e10-72d3-a976-b9df658f7c2f","kind":"session"}},"viewCursor":"v:01a0ee47-6e10-72d3-a976-b9df658f7c2f:7"}}} +{"dir":"in","msg":{"emittedAtMs":1790704057902,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"args":"{\"command_id\":\"phase0-spawn-001\",\"objective\":\"Reply with exactly SUBAGENT_PHASE0_OK. Do not use any tools. Just reply that string.\",\"role\":\"phase0-check\"}","callId":"call_01a0ee477a5c70d6bc8d03c09354a9f2","itemId":"01a0ee47-7e22-7180-a4d7-8b56df20594b","kind":"toolCall","recordedAt":"2026-09-29T17:47:37.901973Z","revision":2,"status":"completed","tool":"subagent_spawn","turnId":"01a0ee47-6e68-7456-ad6f-dde465d1d082","visibleOutput":"{\"status\":\"accepted\",\"subagent_id\":\"01a0ee47-7e25-79f1-b301-6130c0406a84\",\"work_id\":\"work.v2.native_subagent.sha256.03a275518974905bd23efc729849ff7f5da968b6c0005148f5e45bdfe2d1c0fe\",\"agent_path\":\"main/phase0-check/1\",\"task_ref\":\"task/01a0ee47-7e25-79f1-b301-611082261a4d#0\"}"},"sessionId":"01a0ee47-6e10-72d3-a976-b9df658f7c2f","sourceRange":{"first":{"id":"91df9584-990e-4c44-9f90-3356aa275cba","sequence":53},"last":{"id":"f714d6f0-da93-41a7-b272-998f544b35ac","sequence":73},"stream":{"id":"01a0ee47-6e10-72d3-a976-b9df658f7c2f","kind":"session"}},"viewCursor":"v:01a0ee47-6e10-72d3-a976-b9df658f7c2f:8"}}} +{"dir":"in","msg":{"emittedAtMs":1790704059703,"jsonrpc":"2.0","method":"session/tokenUsage","params":{"cumulative":{"outputTokens":386,"promptTokens":76683,"totalTokens":77069},"durationMs":1797,"finishReason":"tool_calls","modelId":"meta/muse-spark-1.3","promptTokens":38532,"sessionId":"01a0ee47-6e10-72d3-a976-b9df658f7c2f","sourceRange":{"first":{"id":"8271b4b1-cc9b-483b-b9ff-fbc08cc309b9","sequence":93},"last":{"id":"8271b4b1-cc9b-483b-b9ff-fbc08cc309b9","sequence":93},"stream":{"id":"01a0ee47-6e10-72d3-a976-b9df658f7c2f","kind":"session"}},"totalTokens":38690,"turnId":"01a0ee47-6e68-7456-ad6f-dde465d1d082","usage":{"cacheReadTokens":38129,"cacheWriteTokens":0,"cachedTokens":38129,"inputTokens":38532,"outputTokens":158,"reasoningTokens":6},"viewCursor":"v:01a0ee47-6e10-72d3-a976-b9df658f7c2f:9"}}} +{"dir":"in","msg":{"emittedAtMs":1790704059704,"jsonrpc":"2.0","method":"item/started","params":{"item":{"args":"{\"command_id\":\"phase0-wait-001\",\"subagent_id\":\"01a0ee47-7e25-79f1-b301-6130c0406a84\",\"timeout_ms\":60000,\"wait_for\":\"result_ready\"}","callId":"call_01a0ee4782007630be7ceef1865c1ec5","itemId":"01a0ee47-8538-7c40-9c16-ef12645a1e6b","kind":"toolCall","recordedAt":"2026-09-29T17:47:39.704685Z","revision":1,"status":"inProgress","tool":"subagent_wait","turnId":"01a0ee47-6e68-7456-ad6f-dde465d1d082"},"sessionId":"01a0ee47-6e10-72d3-a976-b9df658f7c2f","sourceRange":{"first":{"id":"1fc77f96-4d8d-4707-bb35-4402df88d4d2","sequence":95},"last":{"id":"55633f00-93c6-49b4-a592-f9ef36aa3f77","sequence":100},"stream":{"id":"01a0ee47-6e10-72d3-a976-b9df658f7c2f","kind":"session"}},"viewCursor":"v:01a0ee47-6e10-72d3-a976-b9df658f7c2f:10"}}} +{"dir":"in","msg":{"emittedAtMs":1790704060108,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"args":"{\"command_id\":\"phase0-wait-001\",\"subagent_id\":\"01a0ee47-7e25-79f1-b301-6130c0406a84\",\"timeout_ms\":60000,\"wait_for\":\"result_ready\"}","callId":"call_01a0ee4782007630be7ceef1865c1ec5","itemId":"01a0ee47-8538-7c40-9c16-ef12645a1e6b","kind":"toolCall","recordedAt":"2026-09-29T17:47:40.108735Z","revision":2,"status":"completed","tool":"subagent_wait","turnId":"01a0ee47-6e68-7456-ad6f-dde465d1d082","visibleOutput":"{\"status\":\"ready\",\"subagent_id\":\"01a0ee47-7e25-79f1-b301-6130c0406a84\",\"task_ref\":\"task/01a0ee47-7e25-79f1-b301-611082261a4d#5\",\"summary\":\"SUBAGENT_PHASE0_OK\",\"workspace\":{\"placement\":\"shared\",\"setup\":\"active\",\"parent_changes\":\"not_recorded\"},\"evidence_refs\":[\"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee47-6e10-72d3-a976-b9df658f7c2f/subagent/01a0ee47-7e29-7103-97c7-cdde44e26fe8/session.jsonl\"]}"},"sessionId":"01a0ee47-6e10-72d3-a976-b9df658f7c2f","sourceRange":{"first":{"id":"1fc77f96-4d8d-4707-bb35-4402df88d4d2","sequence":95},"last":{"id":"b9435d1c-259c-4332-845c-506b7826a5aa","sequence":110},"stream":{"id":"01a0ee47-6e10-72d3-a976-b9df658f7c2f","kind":"session"}},"viewCursor":"v:01a0ee47-6e10-72d3-a976-b9df658f7c2f:11"}}} +{"dir":"in","msg":{"emittedAtMs":1790704063713,"jsonrpc":"2.0","method":"session/tokenUsage","params":{"cumulative":{"outputTokens":519,"promptTokens":115569,"totalTokens":116088},"durationMs":3600,"finishReason":"tool_calls","modelId":"meta/muse-spark-1.3","promptTokens":38886,"sessionId":"01a0ee47-6e10-72d3-a976-b9df658f7c2f","sourceRange":{"first":{"id":"0d10d7f3-8f3f-4e4e-b3e9-b68fab3d5083","sequence":130},"last":{"id":"0d10d7f3-8f3f-4e4e-b3e9-b68fab3d5083","sequence":130},"stream":{"id":"01a0ee47-6e10-72d3-a976-b9df658f7c2f","kind":"session"}},"totalTokens":39019,"turnId":"01a0ee47-6e68-7456-ad6f-dde465d1d082","usage":{"cacheReadTokens":38513,"cacheWriteTokens":0,"cachedTokens":38513,"inputTokens":38886,"outputTokens":133,"reasoningTokens":29},"viewCursor":"v:01a0ee47-6e10-72d3-a976-b9df658f7c2f:12"}}} +{"dir":"in","msg":{"emittedAtMs":1790704063714,"jsonrpc":"2.0","method":"item/started","params":{"item":{"args":"{\"subagent_id\":\"01a0ee47-7e25-79f1-b301-6130c0406a84\"}","callId":"call_01a0ee4792417281be28207c410311af","itemId":"01a0ee47-94e1-7ac0-a1b6-6bd1dc882dff","kind":"toolCall","recordedAt":"2026-09-29T17:47:43.713973Z","revision":1,"status":"inProgress","tool":"subagent_read_result","turnId":"01a0ee47-6e68-7456-ad6f-dde465d1d082"},"sessionId":"01a0ee47-6e10-72d3-a976-b9df658f7c2f","sourceRange":{"first":{"id":"5e2881f2-1072-4ce0-995e-30afb15179f1","sequence":132},"last":{"id":"864e7062-a1b7-44a6-983c-21580d0978f8","sequence":137},"stream":{"id":"01a0ee47-6e10-72d3-a976-b9df658f7c2f","kind":"session"}},"viewCursor":"v:01a0ee47-6e10-72d3-a976-b9df658f7c2f:13"}}} +{"dir":"in","msg":{"emittedAtMs":1790704063715,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"args":"{\"subagent_id\":\"01a0ee47-7e25-79f1-b301-6130c0406a84\"}","callId":"call_01a0ee4792417281be28207c410311af","itemId":"01a0ee47-94e1-7ac0-a1b6-6bd1dc882dff","kind":"toolCall","recordedAt":"2026-09-29T17:47:43.714985Z","revision":2,"status":"completed","tool":"subagent_read_result","turnId":"01a0ee47-6e68-7456-ad6f-dde465d1d082","visibleOutput":"{\"status\":\"ready\",\"subagent_id\":\"01a0ee47-7e25-79f1-b301-6130c0406a84\",\"task_ref\":\"task/01a0ee47-7e25-79f1-b301-611082261a4d#5\",\"summary\":\"SUBAGENT_PHASE0_OK\",\"workspace\":{\"placement\":\"shared\",\"setup\":\"active\",\"parent_changes\":\"not_recorded\"},\"evidence_refs\":[\"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee47-6e10-72d3-a976-b9df658f7c2f/subagent/01a0ee47-7e29-7103-97c7-cdde44e26fe8/session.jsonl\"]}"},"sessionId":"01a0ee47-6e10-72d3-a976-b9df658f7c2f","sourceRange":{"first":{"id":"5e2881f2-1072-4ce0-995e-30afb15179f1","sequence":132},"last":{"id":"4012cf01-f19e-4a6b-ad34-4e629c95b9cf","sequence":141},"stream":{"id":"01a0ee47-6e10-72d3-a976-b9df658f7c2f","kind":"session"}},"viewCursor":"v:01a0ee47-6e10-72d3-a976-b9df658f7c2f:14"}}} +{"dir":"in","msg":{"emittedAtMs":1790704065625,"jsonrpc":"2.0","method":"item/started","params":{"item":{"itemId":"952ce848-7d20-4b67-b4fa-57d2c468a5a3","kind":"agentMessage","revision":1,"status":"inProgress","text":"","turnId":"01a0ee47-6e68-7456-ad6f-dde465d1d082"},"sessionId":"01a0ee47-6e10-72d3-a976-b9df658f7c2f","viewCursor":"v:01a0ee47-6e10-72d3-a976-b9df658f7c2f:14.1"}}} +{"dir":"in","msg":{"emittedAtMs":1790704065625,"jsonrpc":"2.0","method":"item/delta","params":{"delta":"Subagent replied","field":"text","itemId":"952ce848-7d20-4b67-b4fa-57d2c468a5a3","sessionId":"01a0ee47-6e10-72d3-a976-b9df658f7c2f","viewCursor":"v:01a0ee47-6e10-72d3-a976-b9df658f7c2f:14.2"}}} +{"dir":"in","msg":{"emittedAtMs":1790704065625,"jsonrpc":"2.0","method":"item/delta","params":{"delta":": SUBAGENT_PHASE0_OK.","field":"text","itemId":"952ce848-7d20-4b67-b4fa-57d2c468a5a3","sessionId":"01a0ee47-6e10-72d3-a976-b9df658f7c2f","viewCursor":"v:01a0ee47-6e10-72d3-a976-b9df658f7c2f:14.3"}}} +{"dir":"in","msg":{"emittedAtMs":1790704065675,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"itemId":"952ce848-7d20-4b67-b4fa-57d2c468a5a3","kind":"agentMessage","recordedAt":"2026-09-29T17:47:45.674937Z","revision":2,"status":"completed","text":"Subagent replied: SUBAGENT_PHASE0_OK.","turnId":"01a0ee47-6e68-7456-ad6f-dde465d1d082"},"sessionId":"01a0ee47-6e10-72d3-a976-b9df658f7c2f","sourceRange":{"first":{"id":"ec545d73-530a-443e-89eb-9ca0d190ef81","sequence":158},"last":{"id":"ec545d73-530a-443e-89eb-9ca0d190ef81","sequence":158},"stream":{"id":"01a0ee47-6e10-72d3-a976-b9df658f7c2f","kind":"session"}},"viewCursor":"v:01a0ee47-6e10-72d3-a976-b9df658f7c2f:15"}}} +{"dir":"in","msg":{"emittedAtMs":1790704065679,"jsonrpc":"2.0","method":"session/tokenUsage","params":{"cumulative":{"outputTokens":545,"promptTokens":154786,"totalTokens":155331},"durationMs":1961,"modelId":"meta/muse-spark-1.3","promptTokens":39217,"sessionId":"01a0ee47-6e10-72d3-a976-b9df658f7c2f","sourceRange":{"first":{"id":"f284c974-86d8-4e39-ac2a-3c5ecd3b1a64","sequence":161},"last":{"id":"f284c974-86d8-4e39-ac2a-3c5ecd3b1a64","sequence":161},"stream":{"id":"01a0ee47-6e10-72d3-a976-b9df658f7c2f","kind":"session"}},"totalTokens":39243,"turnId":"01a0ee47-6e68-7456-ad6f-dde465d1d082","usage":{"cacheReadTokens":38769,"cacheWriteTokens":0,"cachedTokens":38769,"inputTokens":39217,"outputTokens":26,"reasoningTokens":5},"viewCursor":"v:01a0ee47-6e10-72d3-a976-b9df658f7c2f:16"}}} +{"dir":"in","msg":{"emittedAtMs":1790704065680,"jsonrpc":"2.0","method":"item/started","params":{"item":{"childSessionId":"5032f8b0-4581-45f5-839f-d90c0cb3124f","childSessionLogPath":"subagent/5032f8b0-4581-45f5-839f-d90c0cb3124f/session.jsonl","fallbackText":"Reminder child session","generationId":1,"itemId":"9e970659-4d27-4fc2-9e4d-b1948d81024f","kind":"reminderChild","recordedAt":"2026-09-29T17:47:45.680658Z","reminderAgentId":"verify-reminder","revision":1,"status":"inProgress","taskId":"01a0ee47-9c90-7712-ba14-50c3bbf3b9b8","turnId":"01a0ee47-6e68-7456-ad6f-dde465d1d082"},"sessionId":"01a0ee47-6e10-72d3-a976-b9df658f7c2f","sourceRange":{"first":{"id":"9e970659-4d27-4fc2-9e4d-b1948d81024f","sequence":168},"last":{"id":"9e970659-4d27-4fc2-9e4d-b1948d81024f","sequence":168},"stream":{"id":"01a0ee47-6e10-72d3-a976-b9df658f7c2f","kind":"session"}},"viewCursor":"v:01a0ee47-6e10-72d3-a976-b9df658f7c2f:17"}}} +{"dir":"in","msg":{"emittedAtMs":1790704068834,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"childSessionId":"5032f8b0-4581-45f5-839f-d90c0cb3124f","childSessionLogPath":"subagent/5032f8b0-4581-45f5-839f-d90c0cb3124f/session.jsonl","fallbackText":"Reminder child session","generationId":1,"itemId":"9e970659-4d27-4fc2-9e4d-b1948d81024f","kind":"reminderChild","recordedAt":"2026-09-29T17:47:48.833854Z","reminderAgentId":"verify-reminder","revision":2,"status":"completed","taskId":"01a0ee47-9c90-7712-ba14-50c3bbf3b9b8","turnId":"01a0ee47-6e68-7456-ad6f-dde465d1d082"},"sessionId":"01a0ee47-6e10-72d3-a976-b9df658f7c2f","sourceRange":{"first":{"id":"9e970659-4d27-4fc2-9e4d-b1948d81024f","sequence":168},"last":{"id":"75117c11-f653-4576-a9cf-cd0baf59f2b0","sequence":170},"stream":{"id":"01a0ee47-6e10-72d3-a976-b9df658f7c2f","kind":"session"}},"viewCursor":"v:01a0ee47-6e10-72d3-a976-b9df658f7c2f:18"}}} +{"dir":"in","msg":{"emittedAtMs":1790704068836,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"childSessionId":"c4256697-9620-414f-816a-dc40d7a841fd","failureReason":"main run completed","fallbackText":"Reminder child session","generationId":1,"itemId":"b084f7ab-fbdf-4518-9977-31762244876b","kind":"reminderChild","recordedAt":"2026-09-29T17:47:48.836755Z","reminderAgentId":"skill-reminder","revision":2,"status":"cancelled","taskId":"01a0ee47-6e71-74c2-914d-8f271c569480","turnId":"01a0ee47-6e68-7456-ad6f-dde465d1d082"},"sessionId":"01a0ee47-6e10-72d3-a976-b9df658f7c2f","sourceRange":{"first":{"id":"b084f7ab-fbdf-4518-9977-31762244876b","sequence":35},"last":{"id":"f4fdf463-ff2e-4ed1-8729-11d708a15078","sequence":174},"stream":{"id":"01a0ee47-6e10-72d3-a976-b9df658f7c2f","kind":"session"}},"viewCursor":"v:01a0ee47-6e10-72d3-a976-b9df658f7c2f:19"}}} +{"dir":"in","msg":{"emittedAtMs":1790704068837,"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a0ee47-6e10-72d3-a976-b9df658f7c2f","status":"idle","viewCursor":"v:01a0ee47-6e10-72d3-a976-b9df658f7c2f:20"}}} +{"dir":"in","msg":{"emittedAtMs":1790704068838,"jsonrpc":"2.0","method":"turn/completed","params":{"durationMs":14969,"sessionId":"01a0ee47-6e10-72d3-a976-b9df658f7c2f","sourceRange":{"first":{"id":"1955daa1-9552-42d8-94a5-73cb1d5d4814","sequence":176},"last":{"id":"1955daa1-9552-42d8-94a5-73cb1d5d4814","sequence":176},"stream":{"id":"01a0ee47-6e10-72d3-a976-b9df658f7c2f","kind":"session"}},"terminal":"completed","timeToFirstTokenMs":11758,"turnId":"01a0ee47-6e68-7456-ad6f-dde465d1d082","viewCursor":"v:01a0ee47-6e10-72d3-a976-b9df658f7c2f:20"}}} +{"dir":"in","msg":{"emittedAtMs":1790704068838,"jsonrpc":"2.0","method":"session/branchChanged","params":{"branch":"main","sessionId":"01a0ee47-6e10-72d3-a976-b9df658f7c2f","sourceRange":{"first":{"id":"f0a5ae9a-f3f7-4c2e-b48b-e339e54c4d3b","sequence":177},"last":{"id":"f0a5ae9a-f3f7-4c2e-b48b-e339e54c4d3b","sequence":177},"stream":{"id":"01a0ee47-6e10-72d3-a976-b9df658f7c2f","kind":"session"}},"vcs":"git","viewCursor":"v:01a0ee47-6e10-72d3-a976-b9df658f7c2f:21","workspaceRoot":"/tmp/muse-phase0/repo"}}} +{"dir":"in","msg":{"emittedAtMs":1790704068838,"jsonrpc":"2.0","method":"session/listChanged","params":{"session":{"activeTurnId":null,"branch":"main","createdAt":"2026-09-29T17:47:33.777159Z","firstUserPrompt":"Spawn one native subagent in this same workspace, without a worktree. Ask it to reply SUBAGENT_PHASE0_OK without tools; wait for it and report its reply.","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee47-6e10-72d3-a976-b9df658f7c2f/session.jsonl","providerId":"meta","sessionId":"01a0ee47-6e10-72d3-a976-b9df658f7c2f","status":"idle","title":"Spawn one native subagent in this same workspace, without a worktree. Ask it to reply SUBAGENT_PHASE0_OK without tools; wait for it and report its reply.","turnCount":1,"updatedAt":"2026-09-29T17:47:48.838179Z","workspaceRoot":"/tmp/muse-phase0/repo"}}}} +{"dir":"in","msg":{"emittedAtMs":1790704069093,"jsonrpc":"2.0","method":"session/closed","params":{"reason":"hostShutdown","sessionId":"01a0ee47-6e10-72d3-a976-b9df658f7c2f","viewCursor":"v:01a0ee47-6e10-72d3-a976-b9df658f7c2f:21"}}} +{"dir":"in","msg":{"emittedAtMs":1790704069093,"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a0ee47-6e10-72d3-a976-b9df658f7c2f","status":"notLoaded","viewCursor":"v:01a0ee47-6e10-72d3-a976-b9df658f7c2f:21"}}} +{"dir":"in","msg":{"emittedAtMs":1790704069093,"jsonrpc":"2.0","method":"session/listChanged","params":{"session":{"activeTurnId":null,"branch":"main","createdAt":"2026-09-29T17:47:33.777159Z","firstUserPrompt":"Spawn one native subagent in this same workspace, without a worktree. Ask it to reply SUBAGENT_PHASE0_OK without tools; wait for it and report its reply.","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee47-6e10-72d3-a976-b9df658f7c2f/session.jsonl","providerId":"meta","sessionId":"01a0ee47-6e10-72d3-a976-b9df658f7c2f","status":"notLoaded","title":"Spawn one native subagent in this same workspace, without a worktree. Ask it to reply SUBAGENT_PHASE0_OK without tools; wait for it and report its reply.","turnCount":1,"updatedAt":"2026-09-29T17:47:48.838179Z","workspaceRoot":"/tmp/muse-phase0/repo"}}}} diff --git a/plugins/muse-provider/test/fixtures/text-reasoning.ndjson b/plugins/muse-provider/test/fixtures/text-reasoning.ndjson new file mode 100644 index 0000000000..e8a4ce16ea --- /dev/null +++ b/plugins/muse-provider/test/fixtures/text-reasoning.ndjson @@ -0,0 +1,53 @@ +{"dir":"out","msg":{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"clientInfo":{"name":"paseo_phase0","version":"0.0.0"},"capabilities":{"experimentalApi":true,"requestedCapabilities":["sessionMcp","sessionListStream"]}}}} +{"dir":"in","msg":{"id":1,"jsonrpc":"2.0","result":{"experimentalApi":true,"grantedCapabilities":["sessionMcp","sessionListStream"],"museHome":"/tmp/muse-phase0/data/muse","platformFamily":"unix","platformOs":"linux","schema":{"fingerprint":"sha256:e0e163db6ccf00dbe68402ce55d6319b3edc33c421f31e9583b587b2de8a118f","version":1},"serverInfo":{"name":"muse","version":"1.4.1"},"sessionDurability":"durable","userAgent":"muse-build/1.4.1 (non-interactive; linux-x86_64; build 35815c253477406321e3f5595195becc25e3907a)"}}} +{"dir":"out","msg":{"jsonrpc":"2.0","method":"initialized","params":{}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":2,"method":"session/start","params":{"commandId":"01a0ee4c-c4f9-74f4-a4d0-8ceb8ed7bd42","workspaceRoot":"/tmp/muse-phase0/repo","modelId":"meta/muse-spark-1.3","providerId":"meta","approvalMode":"onRequest"}}} +{"dir":"in","msg":{"emittedAtMs":1790704403795,"jsonrpc":"2.0","method":"session/started","params":{"session":{"activeTurnId":null,"approvalMode":{"lastCommandId":null,"mode":"onRequest","source":"startup"},"createdAt":"2026-09-29T17:53:23.716151Z","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee4c-c501-7d50-9020-effa9f9956e1/session.jsonl","providerId":"meta","sessionId":"01a0ee4c-c501-7d50-9020-effa9f9956e1","status":"idle","turnCount":0,"updatedAt":"2026-09-29T17:53:23.716155Z","workspaceRoot":"/tmp/muse-phase0/repo"}}}} +{"dir":"in","msg":{"id":2,"jsonrpc":"2.0","result":{"session":{"activeTurnId":null,"approvalMode":{"lastCommandId":null,"mode":"onRequest","source":"startup"},"createdAt":"2026-09-29T17:53:23.716151Z","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee4c-c501-7d50-9020-effa9f9956e1/session.jsonl","providerId":"meta","sessionId":"01a0ee4c-c501-7d50-9020-effa9f9956e1","status":"idle","turnCount":0,"updatedAt":"2026-09-29T17:53:23.716155Z","workspaceRoot":"/tmp/muse-phase0/repo"},"viewCursor":"v:01a0ee4c-c501-7d50-9020-effa9f9956e1:1"}}} +{"dir":"in","msg":{"emittedAtMs":1790704403800,"jsonrpc":"2.0","method":"session/approvalModeChanged","params":{"clientName":"paseo_phase0","commandId":"01a0ee4c-c4f9-74f4-a4d0-8ceb8ed7bd42","mode":"onRequest","sessionId":"01a0ee4c-c501-7d50-9020-effa9f9956e1","source":"approvalReconfigure","sourceRange":{"first":{"id":"f4333176-5846-4643-9db4-716ab148a77a","sequence":5},"last":{"id":"f4333176-5846-4643-9db4-716ab148a77a","sequence":5},"stream":{"id":"01a0ee4c-c501-7d50-9020-effa9f9956e1","kind":"session"}},"viewCursor":"v:01a0ee4c-c501-7d50-9020-effa9f9956e1:1"}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":3,"method":"turn/start","params":{"commandId":"01a0ee4c-c559-75de-a972-7a91dbe59a8e","sessionId":"01a0ee4c-c501-7d50-9020-effa9f9956e1","input":[{"type":"text","text":"Think through the exact rational sum 1 + 1/2 + 1/3 + 1/4 + 1/5 + 1/6. Explain how you reduce it to lowest terms. Do not use tools."}],"reasoningEffort":"high"}}} +{"dir":"in","msg":{"emittedAtMs":1790704403803,"jsonrpc":"2.0","method":"session/branchChanged","params":{"branch":"main","sessionId":"01a0ee4c-c501-7d50-9020-effa9f9956e1","sourceRange":{"first":{"id":"e859ea8a-7b97-468e-af6c-789485c6fb0b","sequence":15},"last":{"id":"e859ea8a-7b97-468e-af6c-789485c6fb0b","sequence":15},"stream":{"id":"01a0ee4c-c501-7d50-9020-effa9f9956e1","kind":"session"}},"vcs":"git","viewCursor":"v:01a0ee4c-c501-7d50-9020-effa9f9956e1:2","workspaceRoot":"/tmp/muse-phase0/repo"}}} +{"dir":"in","msg":{"id":3,"jsonrpc":"2.0","result":{"commandId":"01a0ee4c-c559-75de-a972-7a91dbe59a8e","disposition":"started","startedNewTurn":true,"status":"accepted","turnId":"01a0ee4c-c559-75de-a972-7a91dbe59a8e"}}} +{"dir":"in","msg":{"emittedAtMs":1790704403812,"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a0ee4c-c501-7d50-9020-effa9f9956e1","status":"running","viewCursor":"v:01a0ee4c-c501-7d50-9020-effa9f9956e1:3"}}} +{"dir":"in","msg":{"emittedAtMs":1790704403813,"jsonrpc":"2.0","method":"turn/started","params":{"commandId":"01a0ee4c-c559-75de-a972-7a91dbe59a8e","sessionId":"01a0ee4c-c501-7d50-9020-effa9f9956e1","sourceRange":{"first":{"id":"3294e229-d417-4d6d-b543-31f3a7c72916","sequence":17},"last":{"id":"3294e229-d417-4d6d-b543-31f3a7c72916","sequence":17},"stream":{"id":"01a0ee4c-c501-7d50-9020-effa9f9956e1","kind":"session"}},"turnId":"01a0ee4c-c559-75de-a972-7a91dbe59a8e","viewCursor":"v:01a0ee4c-c501-7d50-9020-effa9f9956e1:3"}}} +{"dir":"in","msg":{"emittedAtMs":1790704403813,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"commandId":"01a0ee4c-c559-75de-a972-7a91dbe59a8e","itemId":"bf516640-ee66-4e83-b101-dc3bae7fd763","kind":"userMessage","recordedAt":"2026-09-29T17:53:23.804051Z","revision":1,"status":"completed","text":"Think through the exact rational sum 1 + 1/2 + 1/3 + 1/4 + 1/5 + 1/6. Explain how you reduce it to lowest terms. Do not use tools.","turnId":"01a0ee4c-c559-75de-a972-7a91dbe59a8e"},"sessionId":"01a0ee4c-c501-7d50-9020-effa9f9956e1","sourceRange":{"first":{"id":"3294e229-d417-4d6d-b543-31f3a7c72916","sequence":17},"last":{"id":"3294e229-d417-4d6d-b543-31f3a7c72916","sequence":17},"stream":{"id":"01a0ee4c-c501-7d50-9020-effa9f9956e1","kind":"session"}},"viewCursor":"v:01a0ee4c-c501-7d50-9020-effa9f9956e1:4"}}} +{"dir":"in","msg":{"emittedAtMs":1790704403813,"jsonrpc":"2.0","method":"item/started","params":{"item":{"childSessionId":"a9db8dd6-6066-4a8b-a41f-04fdfdefdb1a","fallbackText":"Reminder child session","generationId":1,"itemId":"278bcded-5c29-40a1-b034-152609dd21f2","kind":"reminderChild","recordedAt":"2026-09-29T17:53:23.812033Z","reminderAgentId":"skill-reminder","revision":1,"status":"inProgress","taskId":"01a0ee4c-c562-7052-9445-a86270ea242d","turnId":"01a0ee4c-c559-75de-a972-7a91dbe59a8e"},"sessionId":"01a0ee4c-c501-7d50-9020-effa9f9956e1","sourceRange":{"first":{"id":"278bcded-5c29-40a1-b034-152609dd21f2","sequence":35},"last":{"id":"278bcded-5c29-40a1-b034-152609dd21f2","sequence":35},"stream":{"id":"01a0ee4c-c501-7d50-9020-effa9f9956e1","kind":"session"}},"viewCursor":"v:01a0ee4c-c501-7d50-9020-effa9f9956e1:5"}}} +{"dir":"in","msg":{"emittedAtMs":1790704403817,"jsonrpc":"2.0","method":"session/listChanged","params":{"session":{"activeTurnId":"01a0ee4c-c559-75de-a972-7a91dbe59a8e","createdAt":"2026-09-29T17:53:23.716151Z","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee4c-c501-7d50-9020-effa9f9956e1/session.jsonl","providerId":"meta","sessionId":"01a0ee4c-c501-7d50-9020-effa9f9956e1","status":"running","turnCount":0,"updatedAt":"2026-09-29T17:53:23.812401Z","workspaceRoot":"/tmp/muse-phase0/repo"}}}} +{"dir":"in","msg":{"emittedAtMs":1790704409177,"jsonrpc":"2.0","method":"item/started","params":{"item":{"itemId":"f3c7d3fc-1644-46c3-b63c-74e0c7dee6c3","kind":"reasoning","revision":1,"status":"inProgress","summary":[""],"turnId":"01a0ee4c-c559-75de-a972-7a91dbe59a8e"},"sessionId":"01a0ee4c-c501-7d50-9020-effa9f9956e1","viewCursor":"v:01a0ee4c-c501-7d50-9020-effa9f9956e1:5.1"}}} +{"dir":"in","msg":{"emittedAtMs":1790704409177,"jsonrpc":"2.0","method":"item/delta","params":{"delta":"Computing the rational sum by finding a common denominator and reducing the fraction to lowest terms via gcd.","field":"summary.0","itemId":"f3c7d3fc-1644-46c3-b63c-74e0c7dee6c3","sessionId":"01a0ee4c-c501-7d50-9020-effa9f9956e1","viewCursor":"v:01a0ee4c-c501-7d50-9020-effa9f9956e1:5.2"}}} +{"dir":"in","msg":{"emittedAtMs":1790704410350,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"itemId":"f3c7d3fc-1644-46c3-b63c-74e0c7dee6c3","kind":"reasoning","providerItemId":"rs_6abbfb16a0d4c4cc026a42b8:rs_01a0ee4ccf75761a913c4cb237470fa4","recordedAt":"2026-09-29T17:53:30.349608Z","revision":2,"status":"completed","summary":["Computing the rational sum by finding a common denominator and reducing the fraction to lowest terms via gcd."],"turnId":"01a0ee4c-c559-75de-a972-7a91dbe59a8e"},"sessionId":"01a0ee4c-c501-7d50-9020-effa9f9956e1","sourceRange":{"first":{"id":"b1446d39-8f36-461d-9c53-d8aac56413c2","sequence":48},"last":{"id":"b1446d39-8f36-461d-9c53-d8aac56413c2","sequence":48},"stream":{"id":"01a0ee4c-c501-7d50-9020-effa9f9956e1","kind":"session"}},"viewCursor":"v:01a0ee4c-c501-7d50-9020-effa9f9956e1:6"}}} +{"dir":"in","msg":{"emittedAtMs":1790704410350,"jsonrpc":"2.0","method":"item/started","params":{"item":{"itemId":"4fdf781c-c6d0-415a-82ba-dc0ad189d8f6","kind":"agentMessage","revision":1,"status":"inProgress","text":"","turnId":"01a0ee4c-c559-75de-a972-7a91dbe59a8e"},"sessionId":"01a0ee4c-c501-7d50-9020-effa9f9956e1","viewCursor":"v:01a0ee4c-c501-7d50-9020-effa9f9956e1:6.1"}}} +{"dir":"in","msg":{"emittedAtMs":1790704410350,"jsonrpc":"2.0","method":"item/delta","params":{"delta":"Sum with","field":"text","itemId":"4fdf781c-c6d0-415a-82ba-dc0ad189d8f6","sessionId":"01a0ee4c-c501-7d50-9020-effa9f9956e1","viewCursor":"v:01a0ee4c-c501-7d50-9020-effa9f9956e1:6.2"}}} +{"dir":"in","msg":{"emittedAtMs":1790704410350,"jsonrpc":"2.0","method":"item/delta","params":{"delta":" common","field":"text","itemId":"4fdf781c-c6d0-415a-82ba-dc0ad189d8f6","sessionId":"01a0ee4c-c501-7d50-9020-effa9f9956e1","viewCursor":"v:01a0ee4c-c501-7d50-9020-effa9f9956e1:6.3"}}} +{"dir":"in","msg":{"emittedAtMs":1790704410350,"jsonrpc":"2.0","method":"item/delta","params":{"delta":" denominator 60,","field":"text","itemId":"4fdf781c-c6d0-415a-82ba-dc0ad189d8f6","sessionId":"01a0ee4c-c501-7d50-9020-effa9f9956e1","viewCursor":"v:01a0ee4c-c501-7d50-9020-effa9f9956e1:6.4"}}} +{"dir":"in","msg":{"emittedAtMs":1790704410350,"jsonrpc":"2.0","method":"item/delta","params":{"delta":" the LCM of 1 through","field":"text","itemId":"4fdf781c-c6d0-415a-82ba-dc0ad189d8f6","sessionId":"01a0ee4c-c501-7d50-9020-effa9f9956e1","viewCursor":"v:01a0ee4c-c501-7d50-9020-effa9f9956e1:6.5"}}} +{"dir":"in","msg":{"emittedAtMs":1790704410804,"jsonrpc":"2.0","method":"item/delta","params":{"delta":" 6:\n\n* 1 = 60/60\n* 1/","field":"text","itemId":"4fdf781c-c6d0-415a-82ba-dc0ad189d8f6","sessionId":"01a0ee4c-c501-7d50-9020-effa9f9956e1","viewCursor":"v:01a0ee4c-c501-7d50-9020-effa9f9956e1:6.6"}}} +{"dir":"in","msg":{"emittedAtMs":1790704410804,"jsonrpc":"2.0","method":"item/delta","params":{"delta":"2 = 30/60\n* 1/3 = 20/","field":"text","itemId":"4fdf781c-c6d0-415a-82ba-dc0ad189d8f6","sessionId":"01a0ee4c-c501-7d50-9020-effa9f9956e1","viewCursor":"v:01a0ee4c-c501-7d50-9020-effa9f9956e1:6.7"}}} +{"dir":"in","msg":{"emittedAtMs":1790704410804,"jsonrpc":"2.0","method":"item/delta","params":{"delta":"60\n* 1/4 = 15/60\n* 1","field":"text","itemId":"4fdf781c-c6d0-415a-82ba-dc0ad189d8f6","sessionId":"01a0ee4c-c501-7d50-9020-effa9f9956e1","viewCursor":"v:01a0ee4c-c501-7d50-9020-effa9f9956e1:6.8"}}} +{"dir":"in","msg":{"emittedAtMs":1790704410804,"jsonrpc":"2.0","method":"item/delta","params":{"delta":"/5 = 12/60\n* 1/6 = 10","field":"text","itemId":"4fdf781c-c6d0-415a-82ba-dc0ad189d8f6","sessionId":"01a0ee4c-c501-7d50-9020-effa9f9956e1","viewCursor":"v:01a0ee4c-c501-7d50-9020-effa9f9956e1:6.9"}}} +{"dir":"in","msg":{"emittedAtMs":1790704410804,"jsonrpc":"2.0","method":"item/delta","params":{"delta":"/60\n\nSum:\n\n","field":"text","itemId":"4fdf781c-c6d0-415a-82ba-dc0ad189d8f6","sessionId":"01a0ee4c-c501-7d50-9020-effa9f9956e1","viewCursor":"v:01a0ee4c-c501-7d50-9020-effa9f9956e1:6.10"}}} +{"dir":"in","msg":{"emittedAtMs":1790704410888,"jsonrpc":"2.0","method":"item/delta","params":{"delta":"`(","field":"text","itemId":"4fdf781c-c6d0-415a-82ba-dc0ad189d8f6","sessionId":"01a0ee4c-c501-7d50-9020-effa9f9956e1","viewCursor":"v:01a0ee4c-c501-7d50-9020-effa9f9956e1:6.11"}}} +{"dir":"in","msg":{"emittedAtMs":1790704410888,"jsonrpc":"2.0","method":"item/delta","params":{"delta":"60+30+20+15+12+10)/60 = 147","field":"text","itemId":"4fdf781c-c6d0-415a-82ba-dc0ad189d8f6","sessionId":"01a0ee4c-c501-7d50-9020-effa9f9956e1","viewCursor":"v:01a0ee4c-c501-7d50-9020-effa9f9956e1:6.12"}}} +{"dir":"in","msg":{"emittedAtMs":1790704410888,"jsonrpc":"2.0","method":"item/delta","params":{"delta":"/60`\n\nReduce:","field":"text","itemId":"4fdf781c-c6d0-415a-82ba-dc0ad189d8f6","sessionId":"01a0ee4c-c501-7d50-9020-effa9f9956e1","viewCursor":"v:01a0ee4c-c501-7d50-9020-effa9f9956e1:6.13"}}} +{"dir":"in","msg":{"emittedAtMs":1790704410888,"jsonrpc":"2.0","method":"item/delta","params":{"delta":" `147 = 3 \u00d7 7 \u00d7 7`,","field":"text","itemId":"4fdf781c-c6d0-415a-82ba-dc0ad189d8f6","sessionId":"01a0ee4c-c501-7d50-9020-effa9f9956e1","viewCursor":"v:01a0ee4c-c501-7d50-9020-effa9f9956e1:6.14"}}} +{"dir":"in","msg":{"emittedAtMs":1790704410888,"jsonrpc":"2.0","method":"item/delta","params":{"delta":" `60 = 2\u00b2 \u00d7 3 \u00d7 5`, so `","field":"text","itemId":"4fdf781c-c6d0-415a-82ba-dc0ad189d8f6","sessionId":"01a0ee4c-c501-7d50-9020-effa9f9956e1","viewCursor":"v:01a0ee4c-c501-7d50-9020-effa9f9956e1:6.15"}}} +{"dir":"in","msg":{"emittedAtMs":1790704411146,"jsonrpc":"2.0","method":"item/delta","params":{"delta":"gcd(147,60)","field":"text","itemId":"4fdf781c-c6d0-415a-82ba-dc0ad189d8f6","sessionId":"01a0ee4c-c501-7d50-9020-effa9f9956e1","viewCursor":"v:01a0ee4c-c501-7d50-9020-effa9f9956e1:6.16"}}} +{"dir":"in","msg":{"emittedAtMs":1790704411146,"jsonrpc":"2.0","method":"item/delta","params":{"delta":" = 3`.\n\nDivide numerator and denominator by 3:\n\n`147/60","field":"text","itemId":"4fdf781c-c6d0-415a-82ba-dc0ad189d8f6","sessionId":"01a0ee4c-c501-7d50-9020-effa9f9956e1","viewCursor":"v:01a0ee4c-c501-7d50-9020-effa9f9956e1:6.17"}}} +{"dir":"in","msg":{"emittedAtMs":1790704411146,"jsonrpc":"2.0","method":"item/delta","params":{"delta":" = 49/20`\n\n`49 = ","field":"text","itemId":"4fdf781c-c6d0-415a-82ba-dc0ad189d8f6","sessionId":"01a0ee4c-c501-7d50-9020-effa9f9956e1","viewCursor":"v:01a0ee4c-c501-7d50-9020-effa9f9956e1:6.18"}}} +{"dir":"in","msg":{"emittedAtMs":1790704411146,"jsonrpc":"2.0","method":"item/delta","params":{"delta":"7\u00b2` shares","field":"text","itemId":"4fdf781c-c6d0-415a-82ba-dc0ad189d8f6","sessionId":"01a0ee4c-c501-7d50-9020-effa9f9956e1","viewCursor":"v:01a0ee4c-c501-7d50-9020-effa9f9956e1:6.19"}}} +{"dir":"in","msg":{"emittedAtMs":1790704411146,"jsonrpc":"2.0","method":"item/delta","params":{"delta":" no prime","field":"text","itemId":"4fdf781c-c6d0-415a-82ba-dc0ad189d8f6","sessionId":"01a0ee4c-c501-7d50-9020-effa9f9956e1","viewCursor":"v:01a0ee4c-c501-7d50-9020-effa9f9956e1:6.20"}}} +{"dir":"in","msg":{"emittedAtMs":1790704411347,"jsonrpc":"2.0","method":"item/delta","params":{"delta":" factor with `20 = 2\u00b2 \u00d7 5`, so `gcd =","field":"text","itemId":"4fdf781c-c6d0-415a-82ba-dc0ad189d8f6","sessionId":"01a0ee4c-c501-7d50-9020-effa9f9956e1","viewCursor":"v:01a0ee4c-c501-7d50-9020-effa9f9956e1:6.21"}}} +{"dir":"in","msg":{"emittedAtMs":1790704411347,"jsonrpc":"2.0","method":"item/delta","params":{"delta":" 1`. It","field":"text","itemId":"4fdf781c-c6d0-415a-82ba-dc0ad189d8f6","sessionId":"01a0ee4c-c501-7d50-9020-effa9f9956e1","viewCursor":"v:01a0ee4c-c501-7d50-9020-effa9f9956e1:6.22"}}} +{"dir":"in","msg":{"emittedAtMs":1790704411347,"jsonrpc":"2.0","method":"item/delta","params":{"delta":" is in lowest terms.","field":"text","itemId":"4fdf781c-c6d0-415a-82ba-dc0ad189d8f6","sessionId":"01a0ee4c-c501-7d50-9020-effa9f9956e1","viewCursor":"v:01a0ee4c-c501-7d50-9020-effa9f9956e1:6.23"}}} +{"dir":"in","msg":{"emittedAtMs":1790704411393,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"itemId":"4fdf781c-c6d0-415a-82ba-dc0ad189d8f6","kind":"agentMessage","recordedAt":"2026-09-29T17:53:31.392919Z","revision":2,"status":"completed","text":"Sum with common denominator 60, the LCM of 1 through 6:\n\n* 1 = 60/60\n* 1/2 = 30/60\n* 1/3 = 20/60\n* 1/4 = 15/60\n* 1/5 = 12/60\n* 1/6 = 10/60\n\nSum:\n\n`(60+30+20+15+12+10)/60 = 147/60`\n\nReduce: `147 = 3 \u00d7 7 \u00d7 7`, `60 = 2\u00b2 \u00d7 3 \u00d7 5`, so `gcd(147,60) = 3`.\n\nDivide numerator and denominator by 3:\n\n`147/60 = 49/20`\n\n`49 = 7\u00b2` shares no prime factor with `20 = 2\u00b2 \u00d7 5`, so `gcd = 1`. It is in lowest terms.","turnId":"01a0ee4c-c559-75de-a972-7a91dbe59a8e"},"sessionId":"01a0ee4c-c501-7d50-9020-effa9f9956e1","sourceRange":{"first":{"id":"8e63bac4-b43a-435e-82e1-a0b124c9e62e","sequence":50},"last":{"id":"8e63bac4-b43a-435e-82e1-a0b124c9e62e","sequence":50},"stream":{"id":"01a0ee4c-c501-7d50-9020-effa9f9956e1","kind":"session"}},"viewCursor":"v:01a0ee4c-c501-7d50-9020-effa9f9956e1:7"}}} +{"dir":"in","msg":{"emittedAtMs":1790704411420,"jsonrpc":"2.0","method":"session/tokenUsage","params":{"cumulative":{"outputTokens":663,"promptTokens":38169,"totalTokens":38832},"durationMs":7603,"modelId":"meta/muse-spark-1.3","promptTokens":38169,"sessionId":"01a0ee4c-c501-7d50-9020-effa9f9956e1","sourceRange":{"first":{"id":"8533c05b-113f-4e1d-bd04-da179512d45c","sequence":53},"last":{"id":"8533c05b-113f-4e1d-bd04-da179512d45c","sequence":53},"stream":{"id":"01a0ee4c-c501-7d50-9020-effa9f9956e1","kind":"session"}},"totalTokens":38832,"turnId":"01a0ee4c-c559-75de-a972-7a91dbe59a8e","usage":{"cacheReadTokens":0,"cacheWriteTokens":0,"cachedTokens":0,"inputTokens":38169,"outputTokens":663,"reasoningTokens":436},"viewCursor":"v:01a0ee4c-c501-7d50-9020-effa9f9956e1:8"}}} +{"dir":"in","msg":{"emittedAtMs":1790704411422,"jsonrpc":"2.0","method":"item/started","params":{"item":{"childSessionId":"75c1e857-db03-45ea-bc69-9c66f590634a","childSessionLogPath":"subagent/75c1e857-db03-45ea-bc69-9c66f590634a/session.jsonl","fallbackText":"Reminder child session","generationId":1,"itemId":"0cfc2b21-d637-420a-8a3b-f32640a7a031","kind":"reminderChild","recordedAt":"2026-09-29T17:53:31.422285Z","reminderAgentId":"verify-reminder","revision":1,"status":"inProgress","taskId":"01a0ee4c-e31d-75d3-8673-7cb9024888ba","turnId":"01a0ee4c-c559-75de-a972-7a91dbe59a8e"},"sessionId":"01a0ee4c-c501-7d50-9020-effa9f9956e1","sourceRange":{"first":{"id":"0cfc2b21-d637-420a-8a3b-f32640a7a031","sequence":60},"last":{"id":"0cfc2b21-d637-420a-8a3b-f32640a7a031","sequence":60},"stream":{"id":"01a0ee4c-c501-7d50-9020-effa9f9956e1","kind":"session"}},"viewCursor":"v:01a0ee4c-c501-7d50-9020-effa9f9956e1:9"}}} +{"dir":"in","msg":{"emittedAtMs":1790704417085,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"childSessionId":"a9db8dd6-6066-4a8b-a41f-04fdfdefdb1a","fallbackText":"Reminder child session","generationId":1,"itemId":"278bcded-5c29-40a1-b034-152609dd21f2","kind":"reminderChild","recordedAt":"2026-09-29T17:53:37.085045Z","reminderAgentId":"skill-reminder","revision":2,"status":"completed","taskId":"01a0ee4c-c562-7052-9445-a86270ea242d","turnId":"01a0ee4c-c559-75de-a972-7a91dbe59a8e"},"sessionId":"01a0ee4c-c501-7d50-9020-effa9f9956e1","sourceRange":{"first":{"id":"278bcded-5c29-40a1-b034-152609dd21f2","sequence":35},"last":{"id":"82867c05-0d13-41a5-a5f6-5c907e85d4a6","sequence":62},"stream":{"id":"01a0ee4c-c501-7d50-9020-effa9f9956e1","kind":"session"}},"viewCursor":"v:01a0ee4c-c501-7d50-9020-effa9f9956e1:10"}}} +{"dir":"in","msg":{"emittedAtMs":1790704417085,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"childSessionId":"75c1e857-db03-45ea-bc69-9c66f590634a","childSessionLogPath":"subagent/75c1e857-db03-45ea-bc69-9c66f590634a/session.jsonl","fallbackText":"Reminder child session","generationId":1,"itemId":"0cfc2b21-d637-420a-8a3b-f32640a7a031","kind":"reminderChild","recordedAt":"2026-09-29T17:53:37.085573Z","reminderAgentId":"verify-reminder","revision":2,"status":"completed","taskId":"01a0ee4c-e31d-75d3-8673-7cb9024888ba","turnId":"01a0ee4c-c559-75de-a972-7a91dbe59a8e"},"sessionId":"01a0ee4c-c501-7d50-9020-effa9f9956e1","sourceRange":{"first":{"id":"0cfc2b21-d637-420a-8a3b-f32640a7a031","sequence":60},"last":{"id":"41276a5f-1e51-4b0e-8ff5-cd305e2d3a8b","sequence":63},"stream":{"id":"01a0ee4c-c501-7d50-9020-effa9f9956e1","kind":"session"}},"viewCursor":"v:01a0ee4c-c501-7d50-9020-effa9f9956e1:11"}}} +{"dir":"in","msg":{"emittedAtMs":1790704417087,"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a0ee4c-c501-7d50-9020-effa9f9956e1","status":"idle","viewCursor":"v:01a0ee4c-c501-7d50-9020-effa9f9956e1:12"}}} +{"dir":"in","msg":{"emittedAtMs":1790704417088,"jsonrpc":"2.0","method":"turn/completed","params":{"durationMs":13283,"sessionId":"01a0ee4c-c501-7d50-9020-effa9f9956e1","sourceRange":{"first":{"id":"6b97d3d3-e4e3-4c05-a150-58f4bfcfd494","sequence":69},"last":{"id":"6b97d3d3-e4e3-4c05-a150-58f4bfcfd494","sequence":69},"stream":{"id":"01a0ee4c-c501-7d50-9020-effa9f9956e1","kind":"session"}},"terminal":"completed","timeToFirstTokenMs":5373,"turnId":"01a0ee4c-c559-75de-a972-7a91dbe59a8e","viewCursor":"v:01a0ee4c-c501-7d50-9020-effa9f9956e1:12"}}} +{"dir":"in","msg":{"emittedAtMs":1790704417088,"jsonrpc":"2.0","method":"session/branchChanged","params":{"branch":"main","sessionId":"01a0ee4c-c501-7d50-9020-effa9f9956e1","sourceRange":{"first":{"id":"63b58fad-d6e6-4872-8899-802fef5da84a","sequence":70},"last":{"id":"63b58fad-d6e6-4872-8899-802fef5da84a","sequence":70},"stream":{"id":"01a0ee4c-c501-7d50-9020-effa9f9956e1","kind":"session"}},"vcs":"git","viewCursor":"v:01a0ee4c-c501-7d50-9020-effa9f9956e1:13","workspaceRoot":"/tmp/muse-phase0/repo"}}} +{"dir":"in","msg":{"emittedAtMs":1790704417088,"jsonrpc":"2.0","method":"session/listChanged","params":{"session":{"activeTurnId":null,"branch":"main","createdAt":"2026-09-29T17:53:23.713788Z","firstUserPrompt":"Think through the exact rational sum 1 + 1/2 + 1/3 + 1/4 + 1/5 + 1/6. Explain how you reduce it to lowest terms. Do not use tools.","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee4c-c501-7d50-9020-effa9f9956e1/session.jsonl","providerId":"meta","sessionId":"01a0ee4c-c501-7d50-9020-effa9f9956e1","status":"idle","title":"Think through the exact rational sum 1 + 1/2 + 1/3 + 1/4 + 1/5 + 1/6. Explain how you reduce it to lowest terms. Do not use tools.","turnCount":1,"updatedAt":"2026-09-29T17:53:37.088503Z","workspaceRoot":"/tmp/muse-phase0/repo"}}}} +{"dir":"in","msg":{"emittedAtMs":1790704417302,"jsonrpc":"2.0","method":"session/closed","params":{"reason":"hostShutdown","sessionId":"01a0ee4c-c501-7d50-9020-effa9f9956e1","viewCursor":"v:01a0ee4c-c501-7d50-9020-effa9f9956e1:13"}}} +{"dir":"in","msg":{"emittedAtMs":1790704417302,"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a0ee4c-c501-7d50-9020-effa9f9956e1","status":"notLoaded","viewCursor":"v:01a0ee4c-c501-7d50-9020-effa9f9956e1:13"}}} +{"dir":"in","msg":{"emittedAtMs":1790704417302,"jsonrpc":"2.0","method":"session/listChanged","params":{"session":{"activeTurnId":null,"branch":"main","createdAt":"2026-09-29T17:53:23.713788Z","firstUserPrompt":"Think through the exact rational sum 1 + 1/2 + 1/3 + 1/4 + 1/5 + 1/6. Explain how you reduce it to lowest terms. Do not use tools.","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee4c-c501-7d50-9020-effa9f9956e1/session.jsonl","providerId":"meta","sessionId":"01a0ee4c-c501-7d50-9020-effa9f9956e1","status":"notLoaded","title":"Think through the exact rational sum 1 + 1/2 + 1/3 + 1/4 + 1/5 + 1/6. Explain how you reduce it to lowest terms. Do not use tools.","turnCount":1,"updatedAt":"2026-09-29T17:53:37.088503Z","workspaceRoot":"/tmp/muse-phase0/repo"}}}} diff --git a/plugins/muse-provider/test/fixtures/tools-edit.ndjson b/plugins/muse-provider/test/fixtures/tools-edit.ndjson new file mode 100644 index 0000000000..ffbf1a0474 --- /dev/null +++ b/plugins/muse-provider/test/fixtures/tools-edit.ndjson @@ -0,0 +1,48 @@ +{"dir":"out","msg":{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"clientInfo":{"name":"paseo_phase0","version":"0.0.0"},"capabilities":{"experimentalApi":true,"requestedCapabilities":["sessionMcp","sessionListStream"]}}}} +{"dir":"in","msg":{"id":1,"jsonrpc":"2.0","result":{"experimentalApi":true,"grantedCapabilities":["sessionMcp","sessionListStream"],"museHome":"/tmp/muse-phase0/data/muse","platformFamily":"unix","platformOs":"linux","schema":{"fingerprint":"sha256:e0e163db6ccf00dbe68402ce55d6319b3edc33c421f31e9583b587b2de8a118f","version":1},"serverInfo":{"name":"muse","version":"1.4.1"},"sessionDurability":"durable","userAgent":"muse-build/1.4.1 (non-interactive; linux-x86_64; build 35815c253477406321e3f5595195becc25e3907a)"}}} +{"dir":"out","msg":{"jsonrpc":"2.0","method":"initialized","params":{}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":2,"method":"session/start","params":{"commandId":"01a0ee44-d5f5-7f3c-af67-65a9dd4daa28","workspaceRoot":"/tmp/muse-phase0/repo","modelId":"meta/muse-spark-1.3","providerId":"meta","approvalMode":"onRequest"}}} +{"dir":"in","msg":{"emittedAtMs":1790703883853,"jsonrpc":"2.0","method":"session/started","params":{"session":{"activeTurnId":null,"approvalMode":{"lastCommandId":null,"mode":"onRequest","source":"startup"},"createdAt":"2026-09-29T17:44:43.775629Z","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7/session.jsonl","providerId":"meta","sessionId":"01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7","status":"idle","turnCount":0,"updatedAt":"2026-09-29T17:44:43.775633Z","workspaceRoot":"/tmp/muse-phase0/repo"}}}} +{"dir":"in","msg":{"id":2,"jsonrpc":"2.0","result":{"session":{"activeTurnId":null,"approvalMode":{"lastCommandId":null,"mode":"onRequest","source":"startup"},"createdAt":"2026-09-29T17:44:43.775629Z","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7/session.jsonl","providerId":"meta","sessionId":"01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7","status":"idle","turnCount":0,"updatedAt":"2026-09-29T17:44:43.775633Z","workspaceRoot":"/tmp/muse-phase0/repo"},"viewCursor":"v:01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7:1"}}} +{"dir":"in","msg":{"emittedAtMs":1790703883859,"jsonrpc":"2.0","method":"session/approvalModeChanged","params":{"clientName":"paseo_phase0","commandId":"01a0ee44-d5f5-7f3c-af67-65a9dd4daa28","mode":"onRequest","sessionId":"01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7","source":"approvalReconfigure","sourceRange":{"first":{"id":"683cb0f0-a3c9-4d42-b63c-3578223e7bc8","sequence":5},"last":{"id":"683cb0f0-a3c9-4d42-b63c-3578223e7bc8","sequence":5},"stream":{"id":"01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7","kind":"session"}},"viewCursor":"v:01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7:1"}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":3,"method":"turn/start","params":{"commandId":"01a0ee44-d653-785e-adc8-47d6ec011d01","sessionId":"01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7","input":[{"type":"text","text":"Use read_file to read f. Then use bash to run wc -l f. Then use edit_file or apply_patch to change beta to gamma in f. Report the result. Use the requested tools rather than explaining how."}],"reasoningEffort":"low"}}} +{"dir":"in","msg":{"emittedAtMs":1790703883862,"jsonrpc":"2.0","method":"session/branchChanged","params":{"branch":"main","sessionId":"01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7","sourceRange":{"first":{"id":"4bdcc395-645e-46b7-be84-51368193c171","sequence":15},"last":{"id":"4bdcc395-645e-46b7-be84-51368193c171","sequence":15},"stream":{"id":"01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7","kind":"session"}},"vcs":"git","viewCursor":"v:01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7:2","workspaceRoot":"/tmp/muse-phase0/repo"}}} +{"dir":"in","msg":{"id":3,"jsonrpc":"2.0","result":{"commandId":"01a0ee44-d653-785e-adc8-47d6ec011d01","disposition":"started","startedNewTurn":true,"status":"accepted","turnId":"01a0ee44-d653-785e-adc8-47d6ec011d01"}}} +{"dir":"in","msg":{"emittedAtMs":1790703883870,"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7","status":"running","viewCursor":"v:01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7:3"}}} +{"dir":"in","msg":{"emittedAtMs":1790703883871,"jsonrpc":"2.0","method":"turn/started","params":{"commandId":"01a0ee44-d653-785e-adc8-47d6ec011d01","sessionId":"01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7","sourceRange":{"first":{"id":"abb7e5c5-d1ca-4e11-bd29-3c6ff5d8dbed","sequence":17},"last":{"id":"abb7e5c5-d1ca-4e11-bd29-3c6ff5d8dbed","sequence":17},"stream":{"id":"01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7","kind":"session"}},"turnId":"01a0ee44-d653-785e-adc8-47d6ec011d01","viewCursor":"v:01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7:3"}}} +{"dir":"in","msg":{"emittedAtMs":1790703883871,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"commandId":"01a0ee44-d653-785e-adc8-47d6ec011d01","itemId":"ff708436-2703-4465-aa17-9124f646bf18","kind":"userMessage","recordedAt":"2026-09-29T17:44:43.862842Z","revision":1,"status":"completed","text":"Use read_file to read f. Then use bash to run wc -l f. Then use edit_file or apply_patch to change beta to gamma in f. Report the result. Use the requested tools rather than explaining how.","turnId":"01a0ee44-d653-785e-adc8-47d6ec011d01"},"sessionId":"01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7","sourceRange":{"first":{"id":"abb7e5c5-d1ca-4e11-bd29-3c6ff5d8dbed","sequence":17},"last":{"id":"abb7e5c5-d1ca-4e11-bd29-3c6ff5d8dbed","sequence":17},"stream":{"id":"01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7","kind":"session"}},"viewCursor":"v:01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7:4"}}} +{"dir":"in","msg":{"emittedAtMs":1790703883871,"jsonrpc":"2.0","method":"item/started","params":{"item":{"childSessionId":"5030ca0e-e58a-4637-9362-565d8540a2f1","fallbackText":"Reminder child session","generationId":1,"itemId":"985bd509-b773-4afa-ba0b-b64c712d0faf","kind":"reminderChild","recordedAt":"2026-09-29T17:44:43.870303Z","reminderAgentId":"skill-reminder","revision":1,"status":"inProgress","taskId":"01a0ee44-d65c-7732-9968-9dad2d9d011b","turnId":"01a0ee44-d653-785e-adc8-47d6ec011d01"},"sessionId":"01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7","sourceRange":{"first":{"id":"985bd509-b773-4afa-ba0b-b64c712d0faf","sequence":35},"last":{"id":"985bd509-b773-4afa-ba0b-b64c712d0faf","sequence":35},"stream":{"id":"01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7","kind":"session"}},"viewCursor":"v:01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7:5"}}} +{"dir":"in","msg":{"emittedAtMs":1790703883874,"jsonrpc":"2.0","method":"session/listChanged","params":{"session":{"activeTurnId":"01a0ee44-d653-785e-adc8-47d6ec011d01","createdAt":"2026-09-29T17:44:43.775629Z","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7/session.jsonl","providerId":"meta","sessionId":"01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7","status":"running","turnCount":0,"updatedAt":"2026-09-29T17:44:43.870564Z","workspaceRoot":"/tmp/muse-phase0/repo"}}}} +{"dir":"in","msg":{"emittedAtMs":1790703888257,"jsonrpc":"2.0","method":"session/tokenUsage","params":{"cumulative":{"outputTokens":195,"promptTokens":38167,"totalTokens":38362},"durationMs":4383,"finishReason":"tool_calls","modelId":"meta/muse-spark-1.3","promptTokens":38167,"sessionId":"01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7","sourceRange":{"first":{"id":"a7bcb728-487d-449b-a74d-65ae91e96cc0","sequence":51},"last":{"id":"a7bcb728-487d-449b-a74d-65ae91e96cc0","sequence":51},"stream":{"id":"01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7","kind":"session"}},"totalTokens":38362,"turnId":"01a0ee44-d653-785e-adc8-47d6ec011d01","usage":{"cacheReadTokens":0,"cacheWriteTokens":0,"cachedTokens":0,"inputTokens":38167,"outputTokens":195,"reasoningTokens":118},"viewCursor":"v:01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7:6"}}} +{"dir":"in","msg":{"emittedAtMs":1790703888260,"jsonrpc":"2.0","method":"item/started","params":{"item":{"args":"{\"path\":\"f\"}","callId":"call_01a0ee44e5c972c4884239d3e1bcbd3a","itemId":"01a0ee44-e782-7200-948a-362a4b67f285","kind":"toolCall","recordedAt":"2026-09-29T17:44:48.260042Z","revision":1,"status":"inProgress","tool":"read_file","turnId":"01a0ee44-d653-785e-adc8-47d6ec011d01"},"sessionId":"01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7","sourceRange":{"first":{"id":"87892a5b-2955-49e4-8309-390eab4b22bb","sequence":53},"last":{"id":"23208824-203c-4333-8f37-845803185f53","sequence":59},"stream":{"id":"01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7","kind":"session"}},"viewCursor":"v:01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7:7"}}} +{"dir":"in","msg":{"emittedAtMs":1790703888261,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"args":"{\"path\":\"f\"}","callId":"call_01a0ee44e5c972c4884239d3e1bcbd3a","itemId":"01a0ee44-e782-7200-948a-362a4b67f285","kind":"toolCall","recordedAt":"2026-09-29T17:44:48.26126Z","revision":2,"status":"completed","tool":"read_file","turnId":"01a0ee44-d653-785e-adc8-47d6ec011d01","visibleOutput":"Read text file `f`.\n1|alpha\n2|beta"},"sessionId":"01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7","sourceRange":{"first":{"id":"87892a5b-2955-49e4-8309-390eab4b22bb","sequence":53},"last":{"id":"d156c74d-f444-4d33-964e-0a20747b6269","sequence":63},"stream":{"id":"01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7","kind":"session"}},"viewCursor":"v:01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7:8"}}} +{"dir":"in","msg":{"emittedAtMs":1790703891355,"jsonrpc":"2.0","method":"session/tokenUsage","params":{"cumulative":{"outputTokens":280,"promptTokens":76571,"totalTokens":76851},"durationMs":3087,"finishReason":"tool_calls","modelId":"meta/muse-spark-1.3","promptTokens":38404,"sessionId":"01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7","sourceRange":{"first":{"id":"347b7cd4-0568-4fd3-9b66-94fe93d55b97","sequence":82},"last":{"id":"347b7cd4-0568-4fd3-9b66-94fe93d55b97","sequence":82},"stream":{"id":"01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7","kind":"session"}},"totalTokens":38489,"turnId":"01a0ee44-d653-785e-adc8-47d6ec011d01","usage":{"cacheReadTokens":38129,"cacheWriteTokens":0,"cachedTokens":38129,"inputTokens":38404,"outputTokens":85,"reasoningTokens":10},"viewCursor":"v:01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7:9"}}} +{"dir":"in","msg":{"emittedAtMs":1790703891384,"jsonrpc":"2.0","method":"item/started","params":{"item":{"args":"{\"command\":\"wc -l f\",\"description\":\"Count lines in f\"}","callId":"call_01a0ee44ec9671c6a49f55dfb23d281c","itemId":"01a0ee44-f3b6-73b0-8b51-208d485e3bb1","kind":"toolCall","recordedAt":"2026-09-29T17:44:51.384253Z","revision":1,"status":"inProgress","tool":"bash","turnId":"01a0ee44-d653-785e-adc8-47d6ec011d01"},"sessionId":"01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7","sourceRange":{"first":{"id":"9217711f-2891-44a6-aa59-672b82cc28dd","sequence":84},"last":{"id":"a88ea66b-2b42-4699-a692-c76225a52c71","sequence":90},"stream":{"id":"01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7","kind":"session"}},"viewCursor":"v:01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7:10"}}} +{"dir":"in","msg":{"emittedAtMs":1790703891596,"jsonrpc":"2.0","method":"item/delta","params":{"delta":"2 f\n","field":"output","itemId":"01a0ee44-f3b6-73b0-8b51-208d485e3bb1","sessionId":"01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7","viewCursor":"v:01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7:10.1"}}} +{"dir":"in","msg":{"emittedAtMs":1790703891609,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"args":"{\"command\":\"wc -l f\",\"description\":\"Count lines in f\"}","callId":"call_01a0ee44ec9671c6a49f55dfb23d281c","itemId":"01a0ee44-f3b6-73b0-8b51-208d485e3bb1","kind":"toolCall","recordedAt":"2026-09-29T17:44:51.609218Z","revision":2,"status":"completed","tool":"bash","turnId":"01a0ee44-d653-785e-adc8-47d6ec011d01","visibleOutput":"2 f\n"},"sessionId":"01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7","sourceRange":{"first":{"id":"9217711f-2891-44a6-aa59-672b82cc28dd","sequence":84},"last":{"id":"0750216b-86d9-44cc-80d0-2d625f89607c","sequence":96},"stream":{"id":"01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7","kind":"session"}},"viewCursor":"v:01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7:11"}}} +{"dir":"in","msg":{"emittedAtMs":1790703895007,"jsonrpc":"2.0","method":"session/tokenUsage","params":{"cumulative":{"outputTokens":376,"promptTokens":115160,"totalTokens":115536},"durationMs":3390,"finishReason":"tool_calls","modelId":"meta/muse-spark-1.3","promptTokens":38589,"sessionId":"01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7","sourceRange":{"first":{"id":"dc40a59d-2cb8-4f6c-9205-f94f5932849a","sequence":115},"last":{"id":"dc40a59d-2cb8-4f6c-9205-f94f5932849a","sequence":115},"stream":{"id":"01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7","kind":"session"}},"totalTokens":38685,"turnId":"01a0ee44-d653-785e-adc8-47d6ec011d01","usage":{"cacheReadTokens":38385,"cacheWriteTokens":0,"cachedTokens":38385,"inputTokens":38589,"outputTokens":96,"reasoningTokens":11},"viewCursor":"v:01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7:12"}}} +{"dir":"in","msg":{"emittedAtMs":1790703895010,"jsonrpc":"2.0","method":"item/started","params":{"item":{"args":"{\"find\":\"beta\",\"path\":\"f\",\"replace\":\"gamma\"}","callId":"call_01a0ee44fb02709dab9e9c41d70ac7ed","itemId":"01a0ee45-01e0-7bf1-bd88-cc71f76d61df","kind":"toolCall","recordedAt":"2026-09-29T17:44:55.010008Z","revision":1,"status":"inProgress","tool":"edit_file","turnId":"01a0ee44-d653-785e-adc8-47d6ec011d01"},"sessionId":"01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7","sourceRange":{"first":{"id":"ee0c7dc8-42e6-44e9-8278-9b39521f1d69","sequence":117},"last":{"id":"8f6f6999-597e-4d8d-9be4-fe3c8dddf814","sequence":123},"stream":{"id":"01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7","kind":"session"}},"viewCursor":"v:01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7:13"}}} +{"dir":"in","msg":{"emittedAtMs":1790703895014,"jsonrpc":"2.0","method":"item/updated","params":{"item":{"args":"{\"find\":\"beta\",\"path\":\"f\",\"replace\":\"gamma\"}","callId":"call_01a0ee44fb02709dab9e9c41d70ac7ed","itemId":"01a0ee45-01e0-7bf1-bd88-cc71f76d61df","kind":"toolCall","patchRef":{"availability":"available","byteLen":146,"id":"tool_patch-01a0ee45-01e0-7bf1-bd88-cc71f76d61df-call_01a0ee44fb02709dab9e9c41d70ac7ed","kind":"tool_patch","mediaType":"application/json","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7/tool-outputs/01a0ee45-01e0-7bf1-bd88-cc71f76d61df/call_01a0ee44fb02709dab9e9c41d70ac7ed-tool_patch.json","uri":"tool-output://local/01a0ee45-01e0-7bf1-bd88-cc71f76d61df/call_01a0ee44fb02709dab9e9c41d70ac7ed-tool_patch.json"},"patchSummary":{"added":1,"files":1,"removed":1},"recordedAt":"2026-09-29T17:44:55.013611Z","revision":2,"status":"inProgress","tool":"edit_file","turnId":"01a0ee44-d653-785e-adc8-47d6ec011d01"},"sessionId":"01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7","sourceRange":{"first":{"id":"ee0c7dc8-42e6-44e9-8278-9b39521f1d69","sequence":117},"last":{"id":"d1f092a9-296c-4fa0-b2bd-f30acb4be9a4","sequence":126},"stream":{"id":"01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7","kind":"session"}},"viewCursor":"v:01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7:14"}}} +{"dir":"in","msg":{"emittedAtMs":1790703895014,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"args":"{\"find\":\"beta\",\"path\":\"f\",\"replace\":\"gamma\"}","callId":"call_01a0ee44fb02709dab9e9c41d70ac7ed","itemId":"01a0ee45-01e0-7bf1-bd88-cc71f76d61df","kind":"toolCall","patchRef":{"availability":"available","byteLen":146,"id":"tool_patch-01a0ee45-01e0-7bf1-bd88-cc71f76d61df-call_01a0ee44fb02709dab9e9c41d70ac7ed","kind":"tool_patch","mediaType":"application/json","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7/tool-outputs/01a0ee45-01e0-7bf1-bd88-cc71f76d61df/call_01a0ee44fb02709dab9e9c41d70ac7ed-tool_patch.json","uri":"tool-output://local/01a0ee45-01e0-7bf1-bd88-cc71f76d61df/call_01a0ee44fb02709dab9e9c41d70ac7ed-tool_patch.json"},"patchSummary":{"added":1,"files":1,"removed":1},"recordedAt":"2026-09-29T17:44:55.014313Z","revision":3,"status":"completed","tool":"edit_file","turnId":"01a0ee44-d653-785e-adc8-47d6ec011d01","visibleOutput":"edited\nchanged lines: line 2\n--- original\n+++ updated\n@@\n-beta\n+gamma\n"},"sessionId":"01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7","sourceRange":{"first":{"id":"ee0c7dc8-42e6-44e9-8278-9b39521f1d69","sequence":117},"last":{"id":"7137aaae-6f00-4dd7-9e2a-9acc258065c0","sequence":128},"stream":{"id":"01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7","kind":"session"}},"viewCursor":"v:01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7:15"}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":4,"method":"item/readOutput","params":{"sessionId":"01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7","itemId":"01a0ee45-01e0-7bf1-bd88-cc71f76d61df","outputRef":"tool_patch-01a0ee45-01e0-7bf1-bd88-cc71f76d61df-call_01a0ee44fb02709dab9e9c41d70ac7ed"}}} +{"dir":"in","msg":{"id":4,"jsonrpc":"2.0","result":{"byteLen":146,"content":"{\"files\":[{\"hunks\":[{\"lines\":[\" alpha\",\"-beta\",\"+gamma\"],\"newLines\":2,\"newStart\":1,\"oldLines\":2,\"oldStart\":1}],\"path\":\"/tmp/muse-phase0/repo/f\"}]}","encoding":"utf8","eof":true,"mediaType":"application/json","offsetBytes":0}}} +{"dir":"in","msg":{"emittedAtMs":1790703895021,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"childSessionId":"5030ca0e-e58a-4637-9362-565d8540a2f1","fallbackText":"Reminder child session","generationId":1,"itemId":"985bd509-b773-4afa-ba0b-b64c712d0faf","kind":"reminderChild","recordedAt":"2026-09-29T17:44:55.016202Z","reminderAgentId":"skill-reminder","revision":2,"status":"completed","taskId":"01a0ee44-d65c-7732-9968-9dad2d9d011b","turnId":"01a0ee44-d653-785e-adc8-47d6ec011d01"},"sessionId":"01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7","sourceRange":{"first":{"id":"985bd509-b773-4afa-ba0b-b64c712d0faf","sequence":35},"last":{"id":"e0f8e9f0-ba40-4d69-bbc7-4988a886ba7e","sequence":131},"stream":{"id":"01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7","kind":"session"}},"viewCursor":"v:01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7:16"}}} +{"dir":"in","msg":{"emittedAtMs":1790703895021,"jsonrpc":"2.0","method":"item/started","params":{"item":{"childSessionId":"b36c1604-2739-4a03-a07d-82b4fb168b7d","fallbackText":"Reminder child session","generationId":2,"itemId":"7ef38f5d-2cd7-4687-89e9-feea7e217953","kind":"reminderChild","recordedAt":"2026-09-29T17:44:55.018596Z","reminderAgentId":"skill-reminder","revision":1,"status":"inProgress","taskId":"01a0ee45-01e9-7e92-8620-cdf44b6991f9","turnId":"01a0ee44-d653-785e-adc8-47d6ec011d01"},"sessionId":"01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7","sourceRange":{"first":{"id":"7ef38f5d-2cd7-4687-89e9-feea7e217953","sequence":140},"last":{"id":"7ef38f5d-2cd7-4687-89e9-feea7e217953","sequence":140},"stream":{"id":"01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7","kind":"session"}},"viewCursor":"v:01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7:17"}}} +{"dir":"in","msg":{"emittedAtMs":1790703898911,"jsonrpc":"2.0","method":"item/started","params":{"item":{"itemId":"26e5b2e9-c43a-405e-95ef-8257bde46f50","kind":"agentMessage","revision":1,"status":"inProgress","text":"","turnId":"01a0ee44-d653-785e-adc8-47d6ec011d01"},"sessionId":"01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7","viewCursor":"v:01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7:17.1"}}} +{"dir":"in","msg":{"emittedAtMs":1790703898911,"jsonrpc":"2.0","method":"item/delta","params":{"delta":"Done.","field":"text","itemId":"26e5b2e9-c43a-405e-95ef-8257bde46f50","sessionId":"01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7","viewCursor":"v:01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7:17.2"}}} +{"dir":"in","msg":{"emittedAtMs":1790703898912,"jsonrpc":"2.0","method":"item/delta","params":{"delta":" `f","field":"text","itemId":"26e5b2e9-c43a-405e-95ef-8257bde46f50","sessionId":"01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7","viewCursor":"v:01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7:17.3"}}} +{"dir":"in","msg":{"emittedAtMs":1790703898914,"jsonrpc":"2.0","method":"item/delta","params":{"delta":"` had","field":"text","itemId":"26e5b2e9-c43a-405e-95ef-8257bde46f50","sessionId":"01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7","viewCursor":"v:01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7:17.4"}}} +{"dir":"in","msg":{"emittedAtMs":1790703898915,"jsonrpc":"2.0","method":"item/delta","params":{"delta":" 2 lines;","field":"text","itemId":"26e5b2e9-c43a-405e-95ef-8257bde46f50","sessionId":"01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7","viewCursor":"v:01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7:17.5"}}} +{"dir":"in","msg":{"emittedAtMs":1790703898915,"jsonrpc":"2.0","method":"item/delta","params":{"delta":" changed line ","field":"text","itemId":"26e5b2e9-c43a-405e-95ef-8257bde46f50","sessionId":"01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7","viewCursor":"v:01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7:17.6"}}} +{"dir":"in","msg":{"emittedAtMs":1790703898915,"jsonrpc":"2.0","method":"item/delta","params":{"delta":"2 from `beta` to `gamma`.\n","field":"text","itemId":"26e5b2e9-c43a-405e-95ef-8257bde46f50","sessionId":"01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7","viewCursor":"v:01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7:17.7"}}} +{"dir":"in","msg":{"emittedAtMs":1790703899040,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"itemId":"26e5b2e9-c43a-405e-95ef-8257bde46f50","kind":"agentMessage","recordedAt":"2026-09-29T17:44:59.040413Z","revision":2,"status":"completed","text":"Done. `f` had 2 lines; changed line 2 from `beta` to `gamma`.\n","turnId":"01a0ee44-d653-785e-adc8-47d6ec011d01"},"sessionId":"01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7","sourceRange":{"first":{"id":"2b1e63fb-20a2-418f-8c98-1daa937716ff","sequence":153},"last":{"id":"2b1e63fb-20a2-418f-8c98-1daa937716ff","sequence":153},"stream":{"id":"01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7","kind":"session"}},"viewCursor":"v:01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7:18"}}} +{"dir":"in","msg":{"emittedAtMs":1790703899043,"jsonrpc":"2.0","method":"session/tokenUsage","params":{"cumulative":{"outputTokens":411,"promptTokens":153895,"totalTokens":154306},"durationMs":4020,"modelId":"meta/muse-spark-1.3","promptTokens":38735,"sessionId":"01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7","sourceRange":{"first":{"id":"2ccdeaa2-2479-4caf-a4ef-2fe922495ce7","sequence":156},"last":{"id":"2ccdeaa2-2479-4caf-a4ef-2fe922495ce7","sequence":156},"stream":{"id":"01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7","kind":"session"}},"totalTokens":38770,"turnId":"01a0ee44-d653-785e-adc8-47d6ec011d01","usage":{"cacheReadTokens":38513,"cacheWriteTokens":0,"cachedTokens":38513,"inputTokens":38735,"outputTokens":35,"reasoningTokens":3},"viewCursor":"v:01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7:19"}}} +{"dir":"in","msg":{"emittedAtMs":1790703899045,"jsonrpc":"2.0","method":"item/started","params":{"item":{"childSessionId":"b201e8a1-b697-4956-b1a7-9310a260252f","childSessionLogPath":"subagent/b201e8a1-b697-4956-b1a7-9310a260252f/session.jsonl","fallbackText":"Reminder child session","generationId":1,"itemId":"dd0891c7-01b5-452a-bac2-6f85575fee74","kind":"reminderChild","recordedAt":"2026-09-29T17:44:59.045654Z","reminderAgentId":"verify-reminder","revision":1,"status":"inProgress","taskId":"01a0ee45-11a4-7f53-8ade-12c0fc9f640e","turnId":"01a0ee44-d653-785e-adc8-47d6ec011d01"},"sessionId":"01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7","sourceRange":{"first":{"id":"dd0891c7-01b5-452a-bac2-6f85575fee74","sequence":163},"last":{"id":"dd0891c7-01b5-452a-bac2-6f85575fee74","sequence":163},"stream":{"id":"01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7","kind":"session"}},"viewCursor":"v:01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7:20"}}} +{"dir":"in","msg":{"emittedAtMs":1790703909058,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"childSessionId":"b36c1604-2739-4a03-a07d-82b4fb168b7d","fallbackText":"Reminder child session","generationId":2,"itemId":"7ef38f5d-2cd7-4687-89e9-feea7e217953","kind":"reminderChild","recordedAt":"2026-09-29T17:45:09.058336Z","reminderAgentId":"skill-reminder","revision":2,"status":"completed","taskId":"01a0ee45-01e9-7e92-8620-cdf44b6991f9","turnId":"01a0ee44-d653-785e-adc8-47d6ec011d01"},"sessionId":"01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7","sourceRange":{"first":{"id":"7ef38f5d-2cd7-4687-89e9-feea7e217953","sequence":140},"last":{"id":"553a6f63-835e-4e0b-b9ec-a1c72ec585ae","sequence":165},"stream":{"id":"01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7","kind":"session"}},"viewCursor":"v:01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7:21"}}} +{"dir":"in","msg":{"emittedAtMs":1790703909058,"jsonrpc":"2.0","method":"item/completed","params":{"item":{"childSessionId":"b201e8a1-b697-4956-b1a7-9310a260252f","childSessionLogPath":"subagent/b201e8a1-b697-4956-b1a7-9310a260252f/session.jsonl","fallbackText":"Reminder child session","generationId":1,"itemId":"dd0891c7-01b5-452a-bac2-6f85575fee74","kind":"reminderChild","recordedAt":"2026-09-29T17:45:09.058728Z","reminderAgentId":"verify-reminder","revision":2,"status":"completed","taskId":"01a0ee45-11a4-7f53-8ade-12c0fc9f640e","turnId":"01a0ee44-d653-785e-adc8-47d6ec011d01"},"sessionId":"01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7","sourceRange":{"first":{"id":"dd0891c7-01b5-452a-bac2-6f85575fee74","sequence":163},"last":{"id":"10879eda-a61a-464c-a90a-63739193e835","sequence":166},"stream":{"id":"01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7","kind":"session"}},"viewCursor":"v:01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7:22"}}} +{"dir":"in","msg":{"emittedAtMs":1790703909060,"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7","status":"idle","viewCursor":"v:01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7:23"}}} +{"dir":"in","msg":{"emittedAtMs":1790703909061,"jsonrpc":"2.0","method":"turn/completed","params":{"durationMs":25197,"sessionId":"01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7","sourceRange":{"first":{"id":"ab6af672-ea4d-4220-b826-7fcbacdaca74","sequence":172},"last":{"id":"ab6af672-ea4d-4220-b826-7fcbacdaca74","sequence":172},"stream":{"id":"01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7","kind":"session"}},"terminal":"completed","timeToFirstTokenMs":15049,"turnId":"01a0ee44-d653-785e-adc8-47d6ec011d01","viewCursor":"v:01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7:23"}}} +{"dir":"in","msg":{"emittedAtMs":1790703909062,"jsonrpc":"2.0","method":"session/listChanged","params":{"session":{"activeTurnId":null,"branch":"main","createdAt":"2026-09-29T17:44:43.773195Z","firstUserPrompt":"Use read_file to read f. Then use bash to run wc -l f. Then use edit_file or apply_patch to change beta to gamma in f. Report the result. Use the requested tools rather than explaining how.","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7/session.jsonl","providerId":"meta","sessionId":"01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7","status":"idle","title":"Use read_file to read f. Then use bash to run wc -l f. Then use edit_file or apply_patch to change beta to gamma in f. Report the result. Use the requested tools rather than explaining how.","turnCount":1,"updatedAt":"2026-09-29T17:45:09.060175Z","workspaceRoot":"/tmp/muse-phase0/repo"}}}} +{"dir":"in","msg":{"emittedAtMs":1790703909275,"jsonrpc":"2.0","method":"session/closed","params":{"reason":"hostShutdown","sessionId":"01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7","viewCursor":"v:01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7:24"}}} +{"dir":"in","msg":{"emittedAtMs":1790703909275,"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7","status":"notLoaded","viewCursor":"v:01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7:24"}}} +{"dir":"in","msg":{"emittedAtMs":1790703909275,"jsonrpc":"2.0","method":"session/listChanged","params":{"session":{"activeTurnId":null,"branch":"main","createdAt":"2026-09-29T17:44:43.773195Z","firstUserPrompt":"Use read_file to read f. Then use bash to run wc -l f. Then use edit_file or apply_patch to change beta to gamma in f. Report the result. Use the requested tools rather than explaining how.","forkedFrom":null,"modelId":"meta/muse-spark-1.3","path":"/tmp/muse-phase0/data/muse/sessions/2026/09/29/01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7/session.jsonl","providerId":"meta","sessionId":"01a0ee44-d5fc-73a0-8cf8-14a3faa5b4d7","status":"notLoaded","title":"Use read_file to read f. Then use bash to run wc -l f. Then use edit_file or apply_patch to change beta to gamma in f. Report the result. Use the requested tools rather than explaining how.","turnCount":1,"updatedAt":"2026-09-29T17:45:09.061536Z","workspaceRoot":"/tmp/muse-phase0/repo"}}}} diff --git a/plugins/muse-provider/test/fixtures/unsafe-path.ndjson b/plugins/muse-provider/test/fixtures/unsafe-path.ndjson new file mode 100644 index 0000000000..39cf6d7470 --- /dev/null +++ b/plugins/muse-provider/test/fixtures/unsafe-path.ndjson @@ -0,0 +1,5 @@ +{"dir":"out","msg":{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"clientInfo":{"name":"paseo_phase0","version":"0.0.0"},"capabilities":{"experimentalApi":true,"requestedCapabilities":["sessionMcp","sessionListStream"]}}}} +{"dir":"in","msg":{"id":1,"jsonrpc":"2.0","result":{"experimentalApi":true,"grantedCapabilities":["sessionMcp","sessionListStream"],"museHome":"/tmp/muse-phase0/unsafe-data/muse","platformFamily":"unix","platformOs":"linux","schema":{"fingerprint":"sha256:e0e163db6ccf00dbe68402ce55d6319b3edc33c421f31e9583b587b2de8a118f","version":1},"serverInfo":{"name":"muse","version":"1.4.1"},"sessionDurability":"durable","userAgent":"muse-build/1.4.1 (non-interactive; linux-x86_64; build 35815c253477406321e3f5595195becc25e3907a)"}}} +{"dir":"out","msg":{"jsonrpc":"2.0","method":"initialized","params":{}}} +{"dir":"out","msg":{"jsonrpc":"2.0","id":2,"method":"session/start","params":{"commandId":"01a0ee48-f86c-7ea7-ac90-917475456565","workspaceRoot":"/tmp/muse-phase0/repo","modelId":"meta/muse-spark-1.3","approvalMode":"onRequest"}}} +{"dir":"in","msg":{"error":{"code":-32603,"data":{"commandId":"01a0ee48-f86c-7ea7-ac90-917475456565","kind":"internal"},"message":"session/start: read surviving deletion authority: deletion registry authority is unavailable: UnsafePath"},"id":2,"jsonrpc":"2.0"}} diff --git a/plugins/muse-provider/test/provider.test.ts b/plugins/muse-provider/test/provider.test.ts new file mode 100644 index 0000000000..6c060fbfdb --- /dev/null +++ b/plugins/muse-provider/test/provider.test.ts @@ -0,0 +1,1268 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { setTimeout as delay } from "node:timers/promises"; +import os from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { afterEach, expect, test, vi } from "vitest"; +import { + PROVIDER_CAPABILITIES, + ProviderEventSchema, + type ProviderConnection, + type ProviderEvent, + type ProviderRegistration, + type ProviderLaunch, + type ProviderInput, +} from "@getpaseo/plugin/server/provider"; +import { settingsSchema } from "../shared/settings.js"; +import type { UsageSourceRegistration } from "@getpaseo/plugin/server"; +import contribute from "../index.server.js"; + +const connections: ProviderConnection[] = []; +const roots: string[] = []; +afterEach(async () => { + vi.useRealTimers(); + await Promise.all(connections.splice(0).map((connection) => connection.close())); + await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true }))); +}); +function registration( + readSettings = () => settingsSchema.parse({}), + registerUsageSource = (_source: UsageSourceRegistration) => {}, +): ProviderRegistration { + let provider: ProviderRegistration | undefined; + const server = { + registerSettings() { + return { + read: async () => ({ + status: "ready" as const, + revision: "1", + values: readSettings(), + }), + subscribe: () => () => {}, + }; + }, + registerUsageSource, + registerProvider(value: ProviderRegistration) { + provider = value; + }, + }; + contribute(server); + if (!provider) throw new Error("Provider was not registered"); + return provider; +} +async function harness(scenario = "text-reasoning", env: Record = {}) { + const root = await mkdtemp(path.join(os.tmpdir(), "muse-provider-test-")); + roots.push(root); + const requests = path.join(root, "requests.ndjson"); + const launch: ProviderLaunch = { + command: process.execPath, + args: [fileURLToPath(new URL("./fake-muse.mjs", import.meta.url))], + env: { MUSE_TEST_SCENARIO: scenario, MUSE_TEST_REQUESTS: requests, ...env }, + }; + await writeFile(requests, ""); + let settings = settingsSchema.parse({}); + let usageSource!: UsageSourceRegistration; + const provider = registration( + () => settings, + (source) => { + usageSource = source; + }, + ); + const connection = await provider.connect({ + launch, + versions: [1], + capabilities: PROVIDER_CAPABILITIES, + }); + connections.push(connection); + const events: ProviderEvent[] = []; + connection.onEvent((event) => { + events.push(ProviderEventSchema.parse(event)); + }); + async function wait( + predicate: (event: ProviderEvent) => boolean, + from = 0, + ): Promise { + for (let count = 0; count < 200; count++) { + const event = events.slice(from).find(predicate); + if (event) return event; + await delay(10); + } + throw new Error(`Missing event; received ${JSON.stringify(events)}`); + } + async function send(input: ProviderInput) { + const from = events.length; + await connection.send(input); + return from; + } + async function open( + persistence?: Extract["persistence"], + mode = "onRequest", + ) { + await send({ + type: "session.open", + requestId: "open", + sessionId: "paseo-session", + config: { + cwd: root, + env: {}, + settings: {}, + mcpServers: {}, + persist: true, + mode, + model: "meta/muse-spark-1.3", + thinkingOption: "high", + systemPrompt: "Test instructions", + }, + persistence, + history: "replay", + }); + return wait((event) => event.type === "session.ready" || event.type === "request.failed"); + } + async function prompt(delivery: "auto" | "steer" = "auto", image = false) { + return send({ + type: "session.prompt", + sessionId: "paseo-session", + prompt: { + clientMessageId: `client-${events.length}`, + delivery, + input: { + type: "message", + content: image + ? [ + { type: "text", text: "hello" }, + { type: "image", mimeType: "image/png", data: "image-bytes" }, + ] + : [{ type: "text", text: "hello" }], + }, + }, + }); + } + async function recorded() { + return (await readFile(requests, "utf8")) + .trim() + .split("\n") + .filter(Boolean) + .map((line) => JSON.parse(line)); + } + return { + provider, + launch, + connection, + events, + wait, + send, + open, + prompt, + recorded, + root, + usageSource, + setSettings: (values: unknown) => { + settings = settingsSchema.parse(values); + }, + }; +} + +test("registration exposes Muse with a daemon-resolved command", () => { + expect(registration()).toMatchObject({ + id: "muse", + label: "Muse Code", + command: ["muse"], + icon: "icon.svg", + }); +}); +test("catalog uses model effort variants, modes, and bounded cached discovery", async () => { + const h = await harness("catalog-controls"); + await h.send({ type: "catalog", requestId: "catalog-1" }); + const catalogEvent = await h.wait((candidate) => candidate.type === "catalog"); + expect(catalogEvent).toMatchObject({ + catalog: { + models: [ + { + id: "meta/muse-spark-1.3", + label: "meta/muse-spark-1.3", + defaultThinkingOptionId: "high", + thinkingOptions: [ + { id: "minimal" }, + { id: "low" }, + { id: "medium" }, + { id: "high" }, + { id: "xhigh" }, + ], + }, + ], + defaultMode: "onRequest", + modes: [ + { id: "onRequest" }, + { id: "promptUnmatched" }, + { id: "denyUnmatched" }, + { id: "allowAll", isUnattended: true }, + ], + }, + }); + await h.send({ type: "catalog", requestId: "catalog-2" }); + await h.wait((event) => event.type === "catalog" && event.requestId === "catalog-2"); + expect((await h.recorded()).filter((frame) => frame.method === "model/list")).toHaveLength(1); +}); +test("text and reasoning stream, completion overwrites deltas, user echo retains client identity", async () => { + const h = await harness(); + expect(await h.open()).toMatchObject({ type: "session.ready" }); + await h.prompt(); + await h.wait((event) => event.type === "session.turn" && event.state === "completed"); + expect(h.events).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + type: "timeline.item", + item: expect.objectContaining({ type: "reasoning" }), + }), + expect.objectContaining({ + type: "timeline.item", + item: expect.objectContaining({ + type: "user_message", + clientMessageId: expect.stringMatching(/^client-/), + }), + }), + ]), + ); + const frames = await h.recorded(); + expect(frames.find((frame) => frame.method === "turn/start").params).toMatchObject({ + ifBusy: "queue", + reasoningEffort: "high", + displayText: "hello", + input: [ + { type: "text", text: "\nTest instructions\n" }, + { type: "text", text: "hello" }, + ], + }); + expect(h.events.filter((event) => event.type === "session.persistence").length).toBeGreaterThan( + 1, + ); +}); +test("tool calls classify read, shell, and edit with fetched JSON patch converted to unified diff", async () => { + const h = await harness("tools-edit"); + await h.open(); + await h.prompt(); + await h.wait((event) => event.type === "session.turn" && event.state === "completed"); + expect(h.events).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + type: "timeline.item", + item: expect.objectContaining({ + detail: expect.objectContaining({ type: "read", filePath: "f" }), + }), + }), + expect.objectContaining({ + type: "timeline.item", + item: expect.objectContaining({ + detail: expect.objectContaining({ type: "shell", command: "wc -l f", output: "2 f\n" }), + }), + }), + expect.objectContaining({ + type: "timeline.item", + item: expect.objectContaining({ + detail: expect.objectContaining({ + type: "edit", + unifiedDiff: expect.stringContaining("@@ -1,2 +1,2 @@\n alpha\n-beta\n+gamma\n"), + }), + }), + }), + ]), + ); +}); + +test("multi-stage approvals surface Stage 2 then Stage 3 and send the current requirement guard", async () => { + const h = await harness("multi-stage-approval"); + await h.open(); + await h.prompt(); + const first = await h.wait((event) => event.type === "session.permission"); + expect(first).toMatchObject({ + request: { + title: "Stage 2 of 3", + detail: { type: "shell", command: "rm -rf f" }, + actions: [ + { id: "allow_once", behavior: "allow" }, + { id: "allow_local_prefix", behavior: "allow" }, + { id: "abort", behavior: "deny" }, + ], + }, + }); + if (first.type !== "session.permission") throw new Error("Expected approval"); + const from = await h.send({ + type: "session.permission", + sessionId: "paseo-session", + permissionId: first.request.id, + response: { behavior: "allow", selectedActionId: first.request.actions![0]!.id }, + }); + const second = await h.wait((event) => event.type === "session.permission", from); + expect(second).toMatchObject({ + request: { title: "Stage 3 of 3", detail: { command: "rm -rf g" } }, + }); + await h.send({ + type: "session.permission", + sessionId: "paseo-session", + permissionId: first.request.id, + response: { behavior: "allow", selectedActionId: first.request.actions![0]!.id }, + }); + await h.wait((event) => event.type === "session.permission_resolved"); + await h.wait((event) => event.type === "session.turn" && event.state === "completed"); + expect( + (await h.recorded()) + .filter((frame) => frame.method === "approval/decide") + .map((frame) => frame.params.requirementId.sourceIndex), + ).toEqual([1, 2]); +}); +test("Full access automatically answers every escalated stage with the published allow-once choice", async () => { + const h = await harness("multi-stage-approval"); + await h.open(undefined, "allowAll"); + await h.prompt(); + await h.wait((event) => event.type === "session.turn" && event.state === "completed"); + expect(h.events.filter((event) => event.type === "session.permission")).toEqual([]); + expect( + (await h.recorded()) + .filter((frame) => frame.method === "approval/decide") + .map((frame) => frame.params.choiceId), + ).toEqual(["allow_once", "allow_once"]); +}); +test("interrupt awaits cancelled terminal and reconciles the tool", async () => { + const h = await harness("interrupt"); + await h.open(); + await h.prompt(); + await h.wait((event) => event.type === "session.prompt_result"); + await h.send({ type: "session.interrupt", sessionId: "paseo-session", requestId: "interrupt" }); + await h.wait((event) => event.type === "request.completed" && event.requestId === "interrupt"); + const terminalIndex = h.events.findIndex( + (event) => event.type === "session.turn" && event.state === "canceled", + ); + expect(terminalIndex).toBeGreaterThan(-1); + expect( + h.events.findIndex( + (event) => event.type === "request.completed" && event.requestId === "interrupt", + ), + ).toBeGreaterThan(terminalIndex); +}); +for (const cursor of [undefined, "v:old:7"]) { + test(`resume ${cursor ? "with cursor emits suffix and ignores old terminal" : "without cursor replays inline history"}`, async () => { + const h = await harness(cursor ? "resume-with-cursor" : "resume-without-cursor"); + await h.open({ + version: 1, + data: { sessionId: "saved-session", ...(cursor ? { cursor } : {}) }, + }); + expect(h.events.filter((event) => event.type === "session.turn")).toEqual([]); + const history = h.events.filter((event) => event.type === "timeline.item"); + if (!cursor) + expect(history).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + item: expect.objectContaining({ type: "assistant_message", text: "RESUME_MARKER_OK" }), + }), + ]), + ); + await h.prompt(); + await h.wait((event) => event.type === "session.turn" && event.state === "completed"); + expect( + h.events.filter((event) => event.type === "session.turn" && event.state === "completed"), + ).toHaveLength(1); + expect( + (await h.recorded()).find((frame) => frame.method === "session/resume").params, + ).toMatchObject({ sessionId: "saved-session", ...(cursor ? { cursor } : {}) }); + }); +} +test("steer sends ifBusy steer and joins the running turn", async () => { + const h = await harness("steer"); + await h.open(); + await h.prompt(); + await h.wait((event) => event.type === "session.prompt_result"); + const from = await h.prompt("steer"); + expect(await h.wait((event) => event.type === "session.prompt_result", from)).toMatchObject({ + result: { type: "steer" }, + }); + await h.wait((event) => event.type === "session.turn" && event.state === "completed"); + expect( + (await h.recorded()).filter((frame) => frame.method === "turn/start")[1].params.ifBusy, + ).toBe("steer"); +}); +test("image turn sends a native image and the subsequent text turn succeeds", async () => { + const h = await harness("image-followup"); + await h.open(); + await h.prompt("auto", true); + await h.wait((event) => event.type === "session.turn" && event.state === "completed"); + const from = await h.prompt(); + await h.wait((event) => event.type === "session.turn" && event.state === "completed", from); + const turns = (await h.recorded()).filter((frame) => frame.method === "turn/start"); + expect(turns[0].params.input).toContainEqual({ + type: "image", + mediaType: "image/png", + base64Data: "image-bytes", + }); + expect(turns[1].params.input).toEqual([{ type: "text", text: "hello" }]); +}); +for (const source of ["live", "history", "backfill"]) { + test(`reminder housekeeping emits no timeline items or child sessions from ${source}`, async () => { + const scenario = source === "history" ? "resume-without-cursor" : "text-reasoning"; + const frames = ( + await readFile(new URL(`./fixtures/${scenario}.ndjson`, import.meta.url), "utf8") + ) + .trim() + .split("\n") + .map((line) => JSON.parse(line)); + const reminderIds = new Set( + frames + .flatMap(({ msg }) => + msg.params?.item ? [msg.params.item] : (msg.result?.history?.items ?? []), + ) + .filter((item) => item?.kind === "reminderChild") + .map((item) => item.itemId), + ); + expect(reminderIds.size).toBeGreaterThan(0); + const h = await harness(scenario, source === "backfill" ? { MUSE_TEST_GAP: "1" } : {}); + await h.open( + source === "history" ? { version: 1, data: { sessionId: "saved-session" } } : undefined, + ); + await h.prompt(); + await h.wait((event) => event.type === "session.turn" && event.state === "completed"); + expect( + h.events.filter((event) => event.type === "timeline.item" && reminderIds.has(event.item.id)), + ).toEqual([]); + expect( + h.events.filter((event) => event.type === "session.opened").map((event) => event.sessionId), + ).toEqual(["paseo-session"]); + expect((await h.recorded()).filter((frame) => frame.method === "session/read")).toEqual([]); + }); +} +for (const [scenario, code, guidance] of [ + ["auto-review-unavailable", "defaultProfileUnavailable", "permissions.default_profile"], + ["unsafe-path", "unsafePath", path.join("/test-data", "muse")], +]) { + test(`${scenario} is a typed actionable construction error`, async () => { + const h = await harness(scenario, { XDG_DATA_HOME: "/test-data" }); + expect(await h.open()).toMatchObject({ + type: "request.failed", + error: { code, message: expect.stringContaining(guidance!) }, + }); + }); +} +for (const [code, kind] of [ + [0, "cleanExit"], + [2, "usage"], + [3, "credentials"], + [4, "leaseHeld"], + [5, "surfaceOff"], + [17, "crash"], +]) { + test(`exit ${code} is classified ${kind} with stderr diagnostics`, async () => { + const h = await harness("text-reasoning", { MUSE_TEST_EXIT: String(code) }); + expect(await h.open()).toMatchObject({ + type: "request.failed", + error: { code: kind, message: expect.stringContaining("diagnostic tail, not protocol") }, + }); + }); +} +for (const [env, expected] of [ + [ + { MUSE_TEST_VERSION: "1.2.9" }, + { available: false, diagnostic: "Update Muse Code: found 1.2.9, need ≥1.3.0" }, + ], + [ + { MUSE_TEST_ACCOUNT: "loggedOut" }, + { available: false, diagnostic: "Run `muse login` or set META_API_KEY" }, + ], + [{ MUSE_TEST_ACCOUNT: "envKey" }, { available: true }], + [{ MUSE_TEST_ACCOUNT: "apiKey" }, { available: true }], + [{ MUSE_TEST_ACCOUNT: "accountLogin" }, { available: true }], +] as const) { + test(`status checks version and credential state ${JSON.stringify(env)}`, async () => { + const h = await harness("catalog-controls", env); + expect(await h.provider.status!({ launch: h.launch })).toEqual(expected); + }); +} +test("status reports a missing executable", async () => { + const h = await harness(); + expect( + await h.provider.status!({ launch: { ...h.launch, command: "/missing/muse" } }), + ).toMatchObject({ available: false, diagnostic: expect.stringContaining("ENOENT") }); +}); +test("overloaded retry reuses UUIDv7 commandId", async () => { + const h = await harness("text-reasoning", { MUSE_TEST_RETRY: "turn/start" }); + await h.open(); + await h.prompt(); + await h.wait((event) => event.type === "session.turn" && event.state === "completed"); + const turns = (await h.recorded()).filter((frame) => frame.method === "turn/start"); + expect(turns).toHaveLength(2); + expect(turns[0].params.commandId).toBe(turns[1].params.commandId); +}); +test("missing saved session refuses to start fresh", async () => { + const h = await harness("resume-without-cursor", { MUSE_TEST_GONE: "1" }); + expect(await h.open({ version: 1, data: { sessionId: "gone" } })).toMatchObject({ + type: "request.failed", + error: { + code: "sessionNotFound", + message: "This Muse session no longer exists. Create a new agent.", + }, + }); + expect((await h.recorded()).filter((frame) => frame.method === "session/start")).toEqual([]); +}); +test("empty resume viewCursor is a typed error", async () => { + const h = await harness("resume-without-cursor", { MUSE_TEST_EMPTY_CURSOR: "1" }); + expect(await h.open({ version: 1, data: { sessionId: "saved" } })).toMatchObject({ + type: "request.failed", + error: { code: "emptyViewCursor" }, + }); +}); +test("catalogue identity includes command and routing credentials without exposing them", async () => { + const h = await harness(); + const first = await h.provider.getCatalogCacheKey!({ scope: "global", launch: h.launch }); + const second = await h.provider.getCatalogCacheKey!({ + scope: "global", + launch: { ...h.launch, env: { ...h.launch.env, META_API_KEY: "secret" } }, + }); + expect(first).not.toBe(second); + expect(second).not.toContain("secret"); +}); + +test("configure reports the effective model, approval mode, and per-turn effort", async () => { + const h = await harness("catalog-controls"); + await h.open(); + const from = await h.send({ + type: "session.configure", + requestId: "configure", + sessionId: "paseo-session", + changes: { model: "meta/muse-spark-1.3", mode: "promptUnmatched", thinkingOption: "low" }, + }); + await h.wait((event) => event.type === "request.completed" && event.requestId === "configure"); + expect(h.events.slice(from)).toContainEqual( + expect.objectContaining({ + type: "session.config", + config: expect.objectContaining({ + model: "meta/muse-spark-1.3", + mode: "promptUnmatched", + thinkingOption: "low", + models: [expect.objectContaining({ id: "meta/muse-spark-1.3" })], + }), + }), + ); +}); +test("CLI-style allow response selects a published allow action without a supplied id", async () => { + const h = await harness("staged-approval"); + await h.open(); + await h.prompt(); + const permissionEvent = await h.wait((candidate) => candidate.type === "session.permission"); + if (permissionEvent.type !== "session.permission") throw new Error("Expected approval"); + await h.send({ + type: "session.permission", + sessionId: "paseo-session", + permissionId: permissionEvent.request.id, + response: { behavior: "allow" }, + }); + await h.wait((event) => event.type === "session.turn" && event.state === "completed"); + expect( + (await h.recorded()).find((frame) => frame.method === "approval/decide").params.choiceId, + ).toBe("allow_once"); +}); +test("closing cancels a catalogue host instead of waiting for a stalled request", async () => { + const h = await harness("catalog-controls", { MUSE_TEST_HANG: "model/list" }); + await h.send({ type: "catalog", requestId: "catalog" }); + await expect + .poll(async () => (await h.recorded()).some((frame) => frame.method === "model/list")) + .toBe(true); + await h.connection.close(); + expect(h.events.filter((event) => event.type === "catalog")).toEqual([]); + expect(await h.recorded()).toContainEqual({ event: "hostClosed" }); +}); + +for (const retryKind of ["overloaded", "backpressured"]) { + test(`${retryKind} session construction retries with the same UUIDv7`, async () => { + const h = await harness("text-reasoning", { + MUSE_TEST_RETRY: "session/start", + MUSE_TEST_RETRY_KIND: retryKind, + }); + expect(await h.open()).toMatchObject({ type: "session.ready" }); + const attempts = (await h.recorded()).filter((frame) => frame.method === "session/start"); + expect(attempts).toHaveLength(2); + expect(attempts[0].params.commandId).toBe(attempts[1].params.commandId); + }); +} +test("higher-revision item completion replaces streamed text and ignores stale snapshots", async () => { + const h = await harness("text-reasoning", { MUSE_TEST_STALE: "1" }); + await h.open(); + await h.prompt(); + await h.wait((event) => event.type === "session.turn" && event.state === "completed"); + const assistant = h.events.filter( + (event) => event.type === "timeline.item" && event.item.type === "assistant_message", + ); + expect(assistant.length).toBeGreaterThan(2); + expect(assistant.at(-1)).toMatchObject({ + item: { text: expect.stringContaining("`147/60 = 49/20`") }, + }); + expect(assistant).not.toContainEqual( + expect.objectContaining({ item: expect.objectContaining({ text: "STALE_REVISION" }) }), + ); + expect(h.events).toContainEqual( + expect.objectContaining({ + type: "timeline.item", + item: { + type: "reasoning", + id: "f3c7d3fc-1644-46c3-b63c-74e0c7dee6c3", + text: "Computing the rational sum by finding a common denominator and reducing the fraction to lowest terms via gcd.", + }, + }), + ); +}); +test("ordinary subagent tool calls render sub_agent details without inventing child identity", async () => { + const h = await harness("subagent"); + await h.open(); + await h.prompt(); + await h.wait((event) => event.type === "session.turn" && event.state === "completed"); + expect(h.events).toContainEqual( + expect.objectContaining({ + type: "timeline.item", + item: expect.objectContaining({ + name: "subagent_read_result", + status: "completed", + detail: expect.objectContaining({ + type: "sub_agent", + log: expect.stringContaining("SUBAGENT_PHASE0_OK"), + }), + }), + }), + ); +}); +for (const [variants, reasoningEffortVariants, expected] of [ + [["low", "high"], ["medium"], ["medium"]], + [["low", "high"], "unknown", ["low", "high"]], + [[], [], ["minimal", "low", "medium", "high", "xhigh", "max", "ultra"]], +] as const) { + test(`catalogue effort discovery ${JSON.stringify(reasoningEffortVariants)} with variants ${JSON.stringify(variants)}`, async () => { + const model = { + modelId: "test-model", + providerId: "meta", + displayLabel: "Test model", + contextLimit: 123456, + isDefault: true, + variants, + reasoningEffortVariants, + defaultReasoningEffort: "high", + }; + const h = await harness("catalog-controls", { MUSE_TEST_MODELS: JSON.stringify([model]) }); + await h.send({ type: "catalog", requestId: "models" }); + const result = await h.wait((event) => event.type === "catalog"); + expect(result).toMatchObject({ + catalog: { + defaultModel: "test-model", + models: [ + { contextWindowMaxTokens: 123456, thinkingOptions: expected.map((id) => ({ id })) }, + ], + }, + }); + }); +} +test("status accepts future credential states and drains its maintenance host", async () => { + const h = await harness("catalog-controls", { MUSE_TEST_ACCOUNT: "futureCredential" }); + expect(await h.provider.status!({ launch: h.launch })).toEqual({ available: true }); + expect(await h.recorded()).toContainEqual({ event: "hostClosed" }); +}); + +for (const [tool, detail] of [ + ["grep", "search"], + ["glob", "search"], + ["ls", "search"], + ["web_search", "search"], + ["web_fetch", "fetch"], + ["custom_tool", "unknown"], +]) { + test(`${tool} tool calls use ${detail} detail`, async () => { + const h = await harness("tools-edit", { MUSE_TEST_TOOL: tool! }); + await h.open(); + await h.prompt(); + await h.wait((event) => event.type === "session.turn" && event.state === "completed"); + expect(h.events).toContainEqual( + expect.objectContaining({ + type: "timeline.item", + item: expect.objectContaining({ + name: tool, + status: "completed", + detail: expect.objectContaining({ type: detail }), + }), + }), + ); + }); +} +test("a future item kind uses fallbackText without breaking turn completion", async () => { + const h = await harness("tools-edit", { MUSE_TEST_UNKNOWN_KIND: "1" }); + await h.open(); + await h.prompt(); + await h.wait((event) => event.type === "session.turn" && event.state === "completed"); + expect(h.events).toContainEqual( + expect.objectContaining({ + type: "timeline.item", + item: expect.objectContaining({ + name: "futureItem", + detail: { type: "plain_text", label: "futureItem", text: "New MSP item" }, + }), + }), + ); +}); +test("fingerprint mismatch is logged but does not prevent opening", async () => { + const h = await harness("text-reasoning", { MUSE_TEST_FINGERPRINT: "sha256:unverified" }); + expect(await h.open()).toMatchObject({ type: "session.ready" }); +}); +for (const restoring of [false, true]) { + test(`MCP servers map to MSP stdio and streamableHttp on ${restoring ? "resume" : "start"}`, async () => { + const h = await harness(restoring ? "resume-with-cursor" : "text-reasoning"); + await h.send({ + type: "session.open", + sessionId: "paseo-session", + requestId: "mcp", + persistence: restoring + ? { version: 1, data: { sessionId: "saved-session", cursor: "v:old:7" } } + : undefined, + history: "replay", + config: { + cwd: h.root, + env: {}, + persist: true, + settings: {}, + mcpServers: { + stdio: { + type: "stdio", + command: "test-server", + args: ["--serve"], + env: { TEST_ENV: "yes" }, + }, + paseo: { + type: "http", + url: "http://localhost/mcp/agents", + headers: { Authorization: "Bearer test-token" }, + }, + }, + }, + }); + await h.wait((event) => event.type === "session.ready"); + expect( + (await h.recorded()).find( + (frame) => frame.method === (restoring ? "session/resume" : "session/start"), + ).params.config.mcpServers, + ).toEqual({ + stdio: { + transport: "stdio", + command: "test-server", + args: ["--serve"], + env: { TEST_ENV: "yes" }, + framing: "lineDelimitedJson", + }, + paseo: { + transport: "streamableHttp", + url: "http://localhost/mcp/agents", + headers: { Authorization: "Bearer test-token" }, + }, + }); + }); +} + +test("authRequired turn errors retain their kind and explain login recovery", async () => { + const h = await harness("text-reasoning", { MUSE_TEST_AUTH_REQUIRED: "1" }); + await h.open(); + await h.prompt(); + expect( + await h.wait((event) => event.type === "session.turn" && event.state === "failed"), + ).toMatchObject({ + error: { code: "authRequired", message: "Run `muse login` or set META_API_KEY." }, + }); +}); +test("status bounds a stalled account read and closes the host", async () => { + const h = await harness("catalog-controls", { MUSE_TEST_HANG: "account/read" }); + expect(await h.provider.status!({ launch: h.launch })).toEqual({ + available: false, + diagnostic: "Muse account/read timed out", + }); + expect(await h.recorded()).toContainEqual({ event: "hostClosed" }); +}); + +test("interrupt targets the running turn when another prompt is queued", async () => { + const h = await harness("interrupt", { MUSE_TEST_QUEUE: "1" }); + await h.open(); + await h.prompt(); + await h.wait((event) => event.type === "session.prompt_result"); + const from = await h.prompt(); + await h.wait((event) => event.type === "session.prompt_result", from); + await h.send({ + type: "session.interrupt", + sessionId: "paseo-session", + requestId: "interrupt-queued", + }); + await h.wait( + (event) => event.type === "request.completed" && event.requestId === "interrupt-queued", + ); + const requests = await h.recorded(); + const active = requests.find((frame) => frame.method === "turn/start"); + expect(requests.find((frame) => frame.method === "turn/interrupt").params.turnId).toBe( + active.params.commandId, + ); +}); + +for (const terminal of ["completed", "failed"]) { + test(`interrupt resolves when its targeted turn independently ends ${terminal}`, async () => { + const h = await harness("interrupt", { MUSE_TEST_INTERRUPT_TERMINAL: terminal }); + await h.open(); + await h.prompt(); + await h.wait((event) => event.type === "session.prompt_result"); + await h.send({ + type: "session.interrupt", + sessionId: "paseo-session", + requestId: "racing-interrupt", + }); + await h.wait((event) => event.type === "session.turn" && event.state === terminal); + await h.wait( + (event) => event.type === "request.completed" && event.requestId === "racing-interrupt", + ); + expect(h.events.filter((event) => event.type === "request.failed")).toEqual([]); + }); +} +for (const [behavior, choiceId] of [ + ["allow", "allow_once"], + ["deny", "abort"], +] as const) { + test(`CLI ${behavior} prefers the plain once choice over a broader fixture grant`, async () => { + const h = await harness("staged-approval", { MUSE_TEST_CHOICES: "reverse" }); + await h.open(); + await h.prompt(); + const permission = await h.wait((event) => event.type === "session.permission"); + if (permission.type !== "session.permission") throw new Error("Expected permission"); + await h.send({ + type: "session.permission", + sessionId: "paseo-session", + permissionId: permission.request.id, + response: { behavior }, + }); + await expect + .poll( + async () => + (await h.recorded()).find((frame) => frame.method === "approval/decide")?.params.choiceId, + ) + .toBe(choiceId); + }); +} + +test("CLI deny rejects a tool without aborting when denied/once is offered", async () => { + const h = await harness("staged-approval", { MUSE_TEST_DENIED: "1" }); + await h.open(); + await h.prompt(); + const permission = await h.wait((event) => event.type === "session.permission"); + if (permission.type !== "session.permission") throw new Error("Expected permission"); + await h.send({ + type: "session.permission", + sessionId: "paseo-session", + permissionId: permission.request.id, + response: { behavior: "deny" }, + }); + await expect + .poll( + async () => + (await h.recorded()).find((frame) => frame.method === "approval/decide")?.params.choiceId, + ) + .toBe("reject_once"); +}); + +test("skills publish commands and command prompts submit structured skill parts", async () => { + const h = await harness("skill"); + await h.open(); + expect(await h.wait((e) => e.type === "session.commands")).toMatchObject({ + commands: expect.arrayContaining([ + expect.objectContaining({ name: "phase0", description: expect.any(String) }), + expect.objectContaining({ name: "compact", description: expect.any(String) }), + ]), + }); + await h.send({ + type: "session.prompt", + sessionId: "paseo-session", + prompt: { + clientMessageId: "skill-client", + delivery: "auto", + input: { type: "command", name: "phase0", arguments: "argument text" }, + }, + }); + await h.wait((e) => e.type === "session.turn" && e.state === "completed"); + expect((await h.recorded()).find((f) => f.method === "turn/start").params).toMatchObject({ + input: expect.arrayContaining([ + { type: "skill", selector: "phase0", arguments: "argument text" }, + ]), + displayText: "/phase0 argument text", + }); +}); + +test("skill changes refresh the session command catalog", async () => { + const h = await harness("skill", { MUSE_TEST_SKILLS_CHANGED: "1" }); + await h.open(); + const from = await h.prompt(); + expect(await h.wait((e) => e.type === "session.commands", from)).toMatchObject({ + commands: [{ name: "compact", description: "Compact conversation context" }], + }); + expect((await h.recorded()).filter((f) => f.method === "skill/list")).toHaveLength(2); +}); +for (const mode of ["answer", "cancel"] as const) { + test(`todo and question ${mode} replay through the provider interface`, async () => { + const h = await harness(mode === "answer" ? "phase3-controls" : "phase3-cancel"); + await h.open(); + await h.prompt(); + if (mode === "answer") + expect( + await h.wait((e) => e.type === "timeline.item" && e.item.type === "todo"), + ).toMatchObject({ + item: { + items: [ + { text: "inspect f", completed: false, status: "in_progress" }, + { text: "report", completed: false, status: "pending" }, + ], + }, + }); + const permission = await h.wait( + (e) => e.type === "session.permission" && e.request.kind === "question", + ); + if (permission.type !== "session.permission") throw new Error("Expected question"); + expect(permission.request.input).toMatchObject({ + questions: [ + { header: "Color", options: [{ label: "Blue" }, { label: "Green" }], multiSelect: false }, + ], + }); + await h.send({ + type: "session.permission", + sessionId: "paseo-session", + permissionId: permission.request.id, + response: + mode === "answer" + ? { behavior: "allow", updatedInput: { answers: { Color: "Blue" } } } + : { behavior: "deny", message: "QA cancellation" }, + }); + await h.wait( + (e) => e.type === "session.permission_resolved" && e.permissionId === permission.request.id, + ); + const frame = (await h.recorded()).find( + (f) => f.method === (mode === "answer" ? "userInput/answer" : "userInput/cancel"), + ); + expect(frame.params.userInputId).toBe(permission.request.id); + if (mode === "answer") + expect(frame.params.answers).toEqual([{ questionId: "color_choice", selectedLabel: "Blue" }]); + expect((await h.recorded()).some((f) => f.result && Object.keys(f.result).length === 0)).toBe( + true, + ); + }); +} +for (const variant of ["tool", "dedicated", "workflow"]) { + test(`${variant} subagent opens only the supplied child and pages its real transcript`, async () => { + const h = await harness("subagent", { MUSE_TEST_CHILD: variant }); + await h.open(); + await h.prompt(); + expect( + await h.wait((e) => e.type === "session.opened" && e.sessionId === "fixture-child"), + ).toMatchObject({ + parentSessionId: "paseo-session", + restoration: "parent", + toolCallId: expect.any(String), + }); + expect( + await h.wait( + (e) => + e.type === "timeline.item" && + e.sessionId === "fixture-child" && + e.item.type === "assistant_message", + ), + ).toMatchObject({ item: { text: "SUBAGENT_PHASE0_OK" } }); + await h.wait((e) => e.type === "session.turn" && e.state === "completed"); + expect( + h.events.filter((e) => e.type === "session.opened" && e.sessionId === "fixture-child"), + ).toHaveLength(1); + expect( + (await h.recorded()) + .filter((f) => f.method === "session/read") + .map((f) => f.params.sessionId), + ).toEqual(["fixture-child"]); + expect( + h.events.filter( + (e) => + e.type === "timeline.item" && + e.sessionId === "fixture-child" && + e.item.type === "assistant_message", + ), + ).toHaveLength(1); + }); +} +test("gap backfill repairs a missing completion without duplicating item revisions", async () => { + const h = await harness("text-reasoning", { MUSE_TEST_GAP: "1" }); + await h.open(); + await h.prompt(); + await h.wait((e) => e.type === "session.turn" && e.state === "completed"); + const frames = (await h.recorded()).filter((f) => f.method === "view/page"); + expect(frames.map((f) => f.params.cursor)).toEqual(["fixture-last-cursor"]); + const messages = h.events.filter( + (e) => e.type === "timeline.item" && e.item.type === "assistant_message", + ); + expect(messages.at(-1)).toMatchObject({ item: { text: expect.stringContaining("49/20") } }); + expect(h.events.filter((e) => e.type === "session.turn" && e.state === "completed")).toHaveLength( + 1, + ); +}); +test("silent active turns page after two minutes and stop polling on recovered terminal", async () => { + const h = await harness("text-reasoning", { MUSE_TEST_SILENT: "1" }); + await h.open(); + vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout"] }); + await h.prompt(); + await h.wait((e) => e.type === "session.prompt_result"); + await vi.advanceTimersByTimeAsync(119999); + expect((await h.recorded()).filter((f) => f.method === "view/page")).toEqual([]); + await vi.advanceTimersByTimeAsync(1); + await h.wait((e) => e.type === "session.turn" && e.state === "completed"); + await vi.advanceTimersByTimeAsync(240000); + expect((await h.recorded()).filter((f) => f.method === "view/page")).toHaveLength(1); +}); +test("sandbox, network and workspace trust settings apply only on the next open", async () => { + const h = await harness(); + await h.open(); + h.setSettings({ sandbox: false, network: "enabled", trustWorkspace: true }); + expect((await h.recorded()).filter((f) => f.event === "hostStarted").map((f) => f.args)).toEqual([ + ["serve", "--sandbox-network", "proxy-only"], + ]); + await h.send({ type: "session.close", sessionId: "paseo-session", requestId: "close-settings" }); + await h.wait((e) => e.type === "session.closed"); + const from = h.events.length; + await h.open(); + await h.wait((e) => e.type === "session.ready", from); + expect((await h.recorded()).filter((f) => f.event === "hostStarted").map((f) => f.args)).toEqual([ + ["serve", "--sandbox-network", "proxy-only"], + ["serve", "--sandbox-network", "enabled", "--disable-sandbox", "--trust-workspace"], + ]); +}); +test("sessions list filters workspace and imported persistence opens via resume", async () => { + const h = await harness("resume-without-cursor"); + await h.send({ + type: "sessions", + requestId: "import-list", + cwd: "/tmp/muse-phase0/repo", + limit: 2, + }); + const listed = await h.wait((e) => e.type === "sessions"); + if (listed.type !== "sessions") throw new Error("Expected sessions"); + expect(listed.sessions).toHaveLength(2); + expect((await h.recorded()).find((f) => f.method === "session/list").params.workspaceRoot).toBe( + "/tmp/muse-phase0/repo", + ); + await h.open(listed.sessions[0]!.persistence); + expect((await h.recorded()).filter((f) => f.method === "session/start")).toEqual([]); + expect((await h.recorded()).find((f) => f.method === "session/resume").params.sessionId).toBe( + listed.sessions[0]!.persistence.data.sessionId, + ); +}); +for (const fixture of ["phase3-compact", "phase3-compact-history", "phase3-compact-limit"]) { + test(`${fixture} routes compact and exposes the recorded outcome`, async () => { + const h = await harness(fixture); + const rows = ( + await readFile( + fileURLToPath(new URL(`./fixtures/${fixture}.ndjson`, import.meta.url)), + "utf8", + ) + ) + .trim() + .split("\n") + .map((line) => JSON.parse(line)); + const nativeId = rows.find((row) => row.msg.method === "session/resume").msg.params.sessionId; + await h.open({ version: 1, data: { sessionId: nativeId } }); + await h.send({ + type: "session.prompt", + sessionId: "paseo-session", + prompt: { + clientMessageId: "compact", + delivery: "auto", + input: { type: "command", name: "compact", arguments: "" }, + }, + }); + if (fixture === "phase3-compact-limit") { + expect(await h.wait((e) => e.type === "session.prompt_result")).toMatchObject({ + result: { type: "completed" }, + }); + expect( + await h.wait( + (e) => + e.type === "timeline.item" && + e.item.type === "compaction" && + e.item.status === "completed", + ), + ).toMatchObject({ item: { preTokens: 22192 } }); + } else + expect(await h.wait((e) => e.type === "session.prompt_result")).toMatchObject({ + result: { + type: "failed", + error: { + code: "commandRejected", + message: expect.stringContaining("context_compaction.provider_context_limit_tokens"), + }, + }, + }); + expect((await h.recorded()).filter((f) => f.method === "turn/start")).toEqual([]); + }); +} +for (const usage of [ + {}, + { + usage: { + observedAtMs: 1700000000000, + tier: "Pro", + window: { usedPercent: 75, resetsAtMs: 1700018000000, windowDurationMins: 300 }, + weekly: { usedPercent: 105, resetsAtMs: 1700604800000 }, + }, + }, +]) { + test(`usage source presents ${"usage" in usage ? "subscription windows" : "real route absence"} from discovered accounts`, async () => { + const h = await harness("catalog-controls", { MUSE_TEST_USAGE: JSON.stringify(usage) }); + await h.open(); + const inputs = await h.usageSource.discover(); + expect(inputs).toHaveLength(1); + const input = inputs[0]; + expect(await h.usageSource.identify(input)).toEqual({ + key: (input as { account: string }).account, + label: "Muse Code", + }); + const report = await h.usageSource.fetch(input); + if ("usage" in usage) + expect(report).toMatchObject({ + status: "available", + planLabel: "Pro", + windows: [ + { + id: "five_hour", + label: "5 hours", + usedPct: 75, + remainingPct: 25, + tone: "warning", + }, + { id: "weekly", usedPct: 105, remainingPct: 0, tone: "danger" }, + ], + }); + else expect(report).toEqual({ status: "unavailable", windows: [], balances: [], details: [] }); + expect((await h.recorded()).filter((f) => f.method === "usage/read")).toHaveLength(1); + }); +} + +test("a terminal before turn admission does not start an idle watchdog", async () => { + const h = await harness("text-reasoning", { MUSE_TEST_ACK_LAST: "1" }); + await h.open(); + vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout"] }); + await h.prompt(); + await h.wait((e) => e.type === "session.prompt_result"); + await h.wait((e) => e.type === "session.turn" && e.state === "completed"); + await vi.advanceTimersByTimeAsync(120000); + await delay(30); + expect((await h.recorded()).filter((f) => f.method === "view/page")).toEqual([]); +}); +test("admitted compact publishes compaction progress and completes the command without a turn", async () => { + const h = await harness("text-reasoning", { MUSE_TEST_COMPACT: "1" }); + await h.open(); + await h.send({ + type: "session.prompt", + sessionId: "paseo-session", + prompt: { + clientMessageId: "compact-admitted", + delivery: "auto", + input: { type: "command", name: "compact", arguments: "" }, + }, + }); + expect(await h.wait((e) => e.type === "session.prompt_result")).toMatchObject({ + result: { type: "completed" }, + }); + await h.wait( + (e) => + e.type === "timeline.item" && e.item.type === "compaction" && e.item.status === "completed", + ); + expect( + h.events + .filter((e) => e.type === "timeline.item" && e.item.type === "compaction") + .map((e) => e.type === "timeline.item" && e.item), + ).toEqual([ + { type: "compaction", id: "fixture-compaction", status: "loading" }, + { type: "compaction", id: "fixture-compaction", status: "completed" }, + ]); + expect((await h.recorded()).filter((f) => f.method === "turn/start")).toEqual([]); +}); + +test("default workflow children render structured tool details from the real no-flag run", async () => { + const h = await harness("text-reasoning", { MUSE_TEST_WORKFLOW: "1" }); + await h.open(); + await h.prompt(); + const event = await h.wait( + (e) => + e.type === "timeline.item" && + e.item.type === "tool_call" && + e.item.name === "workflow" && + e.item.detail.type === "sub_agent" && + e.item.detail.log?.includes("MUSE_PARITY_CHILD_OK") === true, + ); + expect(event).toMatchObject({ item: { status: "completed", detail: { type: "sub_agent" } } }); + expect(h.events.filter((e) => e.type === "session.opened")).toHaveLength(1); + const workflows = h.events.filter( + (e) => e.type === "timeline.item" && e.item.type === "tool_call" && e.item.name === "workflow", + ); + expect(workflows.length).toBeGreaterThan(1); + for (const workflow of workflows) + expect(workflow).toMatchObject({ item: { detail: { type: "sub_agent" } } }); +}); + +for (const source of ["user", "project"] as const) { + test(`real ${source} skill catalog and command replay with explicit workspace trust`, async () => { + const h = await harness(`phase3-${source}-skill`); + h.setSettings({ trustWorkspace: source === "project" }); + await h.open(); + expect(await h.wait((e) => e.type === "session.commands")).toMatchObject({ + commands: expect.arrayContaining([ + expect.objectContaining({ name: `muse-parity-${source}` }), + ]), + }); + await h.send({ + type: "session.prompt", + sessionId: "paseo-session", + prompt: { + clientMessageId: "real-skill", + delivery: "auto", + input: { type: "command", name: `muse-parity-${source}`, arguments: "QA" }, + }, + }); + await h.wait((e) => e.type === "session.turn" && e.state === "completed"); + expect(h.events).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + type: "timeline.item", + item: expect.objectContaining({ + type: "assistant_message", + text: `MUSE_PARITY_${source.toUpperCase()}_OK`, + }), + }), + ]), + ); + const recorded = await h.recorded(); + expect(recorded.find((f) => f.method === "turn/start").params.input).toContainEqual({ + type: "skill", + selector: `muse-parity-${source}`, + arguments: "QA", + }); + expect(recorded.find((f) => f.event === "hostStarted").args.includes("--trust-workspace")).toBe( + source === "project", + ); + }); +} + +test("usage identity follows the resolved config directory across credential and route changes", async () => { + const identities: string[] = []; + for (const [config, token, route] of [ + ["/usage/config", "token-a", "route-a"], + ["/usage/config", "token-b", "route-b"], + ["/usage/other", "token-b", "route-b"], + ]) { + const h = await harness("catalog-controls", { + XDG_CONFIG_HOME: config!, + META_API_KEY: token!, + META_BASE_URL: route!, + }); + await h.open(); + const inputs = await h.usageSource.discover(); + expect(inputs).toHaveLength(1); + const identity = await h.usageSource.identify(inputs[0]); + expect(identity).not.toBeNull(); + identities.push(identity!.key); + } + expect(identities[0]).toBe(identities[1]); + expect(identities[2]).not.toBe(identities[0]); +}); diff --git a/plugins/tsconfig.json b/plugins/tsconfig.json index 5066af976b..83e35ddb4b 100644 --- a/plugins/tsconfig.json +++ b/plugins/tsconfig.json @@ -7,7 +7,8 @@ "lib": ["ES2023", "DOM"], "types": ["node", "vitest/globals"], "incremental": false, - "noEmit": true + "noEmit": true, + "jsx": "react-jsx" }, "include": ["**/*.ts", "**/*.tsx"], "exclude": ["node_modules", "**/*.test.ts"] diff --git a/public-docs/custom-providers.md b/public-docs/custom-providers.md index 1c89d8d2a6..b325492768 100644 --- a/public-docs/custom-providers.md +++ b/public-docs/custom-providers.md @@ -18,7 +18,9 @@ Everything beyond the [supported providers](/docs/supported-providers) lives und Run `paseo reload` after editing the file. Provider changes apply to future launches without restarting the daemon. -Provider IDs must be lowercase alphanumeric with hyphens (`/^[a-z][a-z0-9-]*$/`). Every custom entry needs `extends` (a first-class provider ID or `"acp"`) and a `label`. +Provider IDs must be lowercase alphanumeric with hyphens (`/^[a-z][a-z0-9-]*$/`). A new custom provider needs `extends` (a first-class provider ID or `"acp"`) and a `label`. +To override a bundled provider plugin, use its provider ID and omit `extends`. For +[Muse Code](/docs/muse-code), set command or environment overrides under `agents.providers.muse`. The examples below are a quick tour. The full, up-to-date reference is on GitHub: [docs/custom-providers.md](https://github.com/getpaseo/paseo/blob/main/docs/custom-providers.md). diff --git a/public-docs/docker.md b/public-docs/docker.md index f6324a09f2..6f19da692a 100644 --- a/public-docs/docker.md +++ b/public-docs/docker.md @@ -39,7 +39,7 @@ The image: - stores daemon state under `/home/paseo/.paseo` - runs the daemon and launched agents as the non-root `paseo` user -The image does not bundle agent CLIs such as Claude Code, Codex, OpenCode, Copilot, or Pi. Add the agents you use with a small child image. +The image does not bundle agent CLIs such as Claude Code, Codex, OpenCode, Copilot, Pi, or Muse Code. Add the agents you use with a small child image. Host-side CLI commands select the container explicitly, for example `paseo project ls --host 127.0.0.1:6767`. Without an endpoint selector the CLI looks for a local home’s supervisor. Container environment settings are deployment overrides; worker restart preserves them. Your container manager owns full supervisor replacement. diff --git a/public-docs/muse-code.md b/public-docs/muse-code.md new file mode 100644 index 0000000000..99c3096755 --- /dev/null +++ b/public-docs/muse-code.md @@ -0,0 +1,58 @@ +--- +title: Muse Code +description: Install and configure Muse Code for Paseo. +nav: Muse Code +order: 25 +category: Providers +--- + +# Muse Code + +Run Meta's terminal coding agent in Paseo with your existing Muse installation and credentials. + +## Getting started + +Install Muse Code on the machine running the Paseo daemon: + +```bash +curl -fsSL https://dev.meta.ai/install.sh | sh +muse login +``` + +Use Muse 1.3.0 or newer. This integration was verified with 1.4.1. For API access, set +`META_API_KEY` in the daemon's environment instead of logging in. See +[provider environment overrides](/docs/custom-providers). + +In Paseo, select **Muse Code**, then choose a model, approval mode, and reasoning effort. +Paseo runs `muse serve` and communicates over the Muse Session Protocol (MSP). Your Muse +configuration determines the available models. + +## Plugin settings + +Open Command Center and select **Configure Muse Code**. Changes apply when an agent's +Muse host next opens, including a refresh of an existing agent. + +| Setting | Default | Effect | +| --------------- | ------------ | ----------------------------------------------------------------------------------------------- | +| Sandbox | On | Keeps Muse's filesystem and network sandbox enabled. Turning it off allows full network access. | +| Network | `proxy-only` | Muse's sandbox network policy: `proxy-only`, `restricted`, or `enabled`. | +| Trust workspace | Off | Lets Muse load project-scoped skills and configuration. | + +Approval modes control tool decisions separately: **Default**, **Ask**, **Strict**, and +**Full access**. Full access automatically allows escalated approval stages; it keeps the +sandbox setting you chose. Skills appear in the slash-command menu, alongside `/compact`. + +## Limitations in Muse 1.4.1 + +- **Paseo MCP tools are unavailable.** Muse drops the returned tool namespace before matching + an MCP call. This upstream issue must be fixed in Muse before its agents can use Paseo tools. +- **Compaction needs a model context limit.** If your endpoint does not report one, configure + `context_compaction.provider_context_limit_tokens` in Muse for the selected model. +- **Usage windows appear only when Muse reports them.** An empty `usage/read` response + leaves subscription usage unavailable. +- **Native subagents may need Muse's experimental SDK flag.** Set + `MUSE_EXPERIMENTAL_SDK_ENABLED=on` in the provider environment if native subagent tools + are missing. Linked child sessions appear only when Muse supplies `childSessionId`. + +See [custom providers](/docs/custom-providers) to override the binary or environment under +`agents.providers.muse`. diff --git a/public-docs/providers.md b/public-docs/providers.md index ea7f09e36e..6de304a019 100644 --- a/public-docs/providers.md +++ b/public-docs/providers.md @@ -8,7 +8,7 @@ category: Providers # Providers -Paseo doesn't ship its own coding agent. It launches and supervises **existing CLIs you've already installed and authenticated**, Claude Code, Codex, OpenCode, Cursor, Gemini, and the rest. Your subscriptions, your config, your skills, your MCP servers all stay intact. Paseo just gives you a UI, a CLI, a relay, and orchestration on top. +Paseo doesn't ship its own coding agent. It launches and supervises **existing CLIs you've already installed and authenticated**, Claude Code, Codex, OpenCode, Antigravity, Muse Code, Cursor, Gemini, and the rest. Your subscriptions, your config, your skills, your MCP servers all stay intact. Paseo just gives you a UI, a CLI, a relay, and orchestration on top. ## Mental model @@ -16,7 +16,7 @@ A provider is the contract between Paseo and one external agent CLI: how to laun ## Two tiers -- **Native support**, Paseo ships a bundled adapter for the major agents (Claude Code, Codex, OpenCode, pi). Auto-discovered when the underlying CLI is installed, with mode metadata and voice support where applicable. +- **Native support**, Paseo ships a bundled adapter for the major agents (Claude Code, Codex, OpenCode, Pi, [Antigravity](/docs/supported-providers#antigravity), [Muse Code](/docs/muse-code)). Auto-discovered when the underlying CLI is installed, with mode metadata and voice support where applicable. - **ACP catalog**, any agent speaking the [Agent Client Protocol](https://agentclientprotocol.com) is supported through a generic adapter. Paseo ships a curated catalog of one-click installs (Cursor, Gemini, GitHub Copilot, Hermes, Kimi, Qwen Code, and 25+ more), and you can add any other ACP agent yourself. Either way, **you install the underlying CLI**. Paseo runs it. diff --git a/public-docs/security.md b/public-docs/security.md index 982b956829..a935a75878 100644 --- a/public-docs/security.md +++ b/public-docs/security.md @@ -123,7 +123,7 @@ See [Docker](/docs/docker) for Compose and reverse proxy examples. ## Agent authentication -Paseo wraps agent CLIs (Claude Code, Codex, OpenCode) but does not manage their authentication. Each agent provider handles its own credentials: +Paseo wraps agent CLIs (Claude Code, Codex, OpenCode, Muse Code) but does not manage their authentication. Each agent provider handles its own credentials: - **Claude Code**, authenticates via Anthropic's OAuth flow, stored in `~/.claude/` - **Codex**, uses your OpenAI API key or OAuth session diff --git a/public-docs/supported-providers.md b/public-docs/supported-providers.md index 588fd96195..aaed86f5c4 100644 --- a/public-docs/supported-providers.md +++ b/public-docs/supported-providers.md @@ -19,6 +19,7 @@ Work out of the box once the underlying CLI is installed and authenticated. - [OpenCode](https://opencode.ai/). Open-source coding assistant with multi-provider model support. - [Pi](https://pi.dev). Minimal terminal-based coding agent with multi-provider LLM support. - [Antigravity](#antigravity). Uses your installed `agy` CLI through a built-in provider plugin. +- [Muse Code](/docs/muse-code). Meta's terminal coding agent, bundled as a provider plugin. ### Antigravity diff --git a/scripts/builtin-plugins-dist.test.mjs b/scripts/builtin-plugins-dist.test.mjs index 7ec6ba55ee..acaa684a44 100644 --- a/scripts/builtin-plugins-dist.test.mjs +++ b/scripts/builtin-plugins-dist.test.mjs @@ -1,6 +1,7 @@ import assert from "node:assert/strict"; -import { readFile, stat } from "node:fs/promises"; +import { mkdtemp, readFile, rm, stat } from "node:fs/promises"; import path from "node:path"; +import os from "node:os"; import test from "node:test"; import { fileURLToPath } from "node:url"; import pino from "pino"; @@ -28,7 +29,8 @@ test("built output compiles and starts every listed built-in", async () => { const { version } = JSON.parse( await readFile(path.join(repoRoot, "packages/server/package.json"), "utf8"), ); - const runtime = new PluginRuntime(pino({ level: "silent" }), version); + const settingsDirectory = await mkdtemp(path.join(os.tmpdir(), "builtin-dist-settings-")); + const runtime = new PluginRuntime(pino({ level: "silent" }), version, { settingsDirectory }); runtime.bindPaseoSessionHost({ async attachPluginSocket(_pluginId, socket) { const closed = new Promise((resolve) => socket.once("close", resolve)); @@ -76,5 +78,6 @@ test("built output compiles and starts every listed built-in", async () => { } } finally { await runtime.stopAll(); + await rm(settingsDirectory, { recursive: true, force: true }); } }); From 4417d7c47a7bec3aa37abb2be00afb0b1dbe8797 Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Wed, 30 Sep 2026 16:08:18 +0200 Subject: [PATCH 024/100] fix(pi): give Pi's built-in MCP the agent's MCP servers (#5762) Pi 0.99 ships MCP as a built-in extension. Paseo only recognized pi-mcp-adapter's /mcp command, so a Pi agent without the adapter started without Paseo's MCP servers. Pi 0.99 also rejects the adapter's --mcp-config flag, so the servers are now registered with pi.registerMcpServer from Paseo's per-session extension. Sessions that load pi-mcp-adapter keep the --mcp-config path. Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> Co-authored-by: ArietidsZ <69024461+ArietidsZ@users.noreply.github.com> --- docs/providers.md | 4 +- .../server/agent/providers/pi/agent.test.ts | 107 +++++++++++++ .../src/server/agent/providers/pi/agent.ts | 146 +++++++++++++----- 3 files changed, 214 insertions(+), 43 deletions(-) diff --git a/docs/providers.md b/docs/providers.md index 917c2512d1..6373caa57c 100644 --- a/docs/providers.md +++ b/docs/providers.md @@ -103,7 +103,9 @@ Paseo's per-agent and daemon-wide system prompts are appended by its generated P Pi model records expose input capabilities through `model.input`. Only send raw RPC `images` when the current model explicitly includes `"image"` in that list. Text-only Pi/OMP models reject image content and persist the rejected image in JSONL history, so image prompts for those models must be materialized to a local file and passed as a text path hint instead. -Pi MCP support depends on the open-source `pi-mcp-adapter` extension being loaded for the agent cwd. Probe with Pi RPC `get_commands`; the adapter registers an extension command named `mcp` (often with `sourceInfo.source` containing `pi-mcp-adapter`). When Paseo injects MCP servers into Pi, write a per-agent MCP config and pass it with `--mcp-config` instead of modifying user or project MCP files. Because that flag replaces the Pi global config layer, preserve the existing `/mcp.json` in the generated file before overlaying injected servers. For local HTTP servers such as Paseo's own `/mcp/agents` endpoint, explicitly disable adapter OAuth (`auth: false`, `oauth: false`) in the generated config. +Probe Pi MCP support with Pi RPC `get_commands` for the agent cwd. Pi 0.99 and later ship MCP as a built-in extension, which registers an extension command named `mcp` with `sourceInfo.path` `builtin:mcp`. Register injected servers through `pi.registerMcpServer` in Paseo's generated extension; the built-in extension rejects SSE servers and names with characters other than letters, digits, `_`, and `-`, and a server of the same name in Pi's `mcp.json` takes precedence. + +The open-source `pi-mcp-adapter` extension replaces the built-in one and registers its own `mcp` command (often with `sourceInfo.source` containing `pi-mcp-adapter`). When it is loaded, write a per-agent MCP config and pass it with `--mcp-config` instead of modifying user or project MCP files. Because that flag replaces the Pi global config layer, preserve the existing `/mcp.json` in the generated file before overlaying injected servers. For local HTTP servers such as Paseo's own `/mcp/agents` endpoint, explicitly disable adapter OAuth (`auth: false`, `oauth: false`) in the generated config. Pi control-plane RPCs wait 60 seconds by default. Override `params.rpcTimeoutMs` when extension or MCP startup on a slow host needs more time. Timeout errors name the pending RPC phase and report both elapsed time and the configured deadline. This setting does not govern long-running Pi compaction or Pi extension UI results. See [OMP profiles and Pi-compatible forks](custom-providers.md#omp-profiles-and-pi-compatible-forks) for OMP startup and RPC deadlines. diff --git a/packages/server/src/server/agent/providers/pi/agent.test.ts b/packages/server/src/server/agent/providers/pi/agent.test.ts index 0130574211..29c515ad76 100644 --- a/packages/server/src/server/agent/providers/pi/agent.test.ts +++ b/packages/server/src/server/agent/providers/pi/agent.test.ts @@ -160,6 +160,7 @@ function parseEntryCapture(notification: string): unknown { async function loadPaseoExtensionListeners( extensionPath: string, + registerMcpServer: (name: string, config: unknown) => void = () => undefined, ): Promise> { const listeners = new Map(); const extension = (await import(pathToFileURL(extensionPath).href)) as { @@ -167,16 +168,26 @@ async function loadPaseoExtensionListeners( on: (event: string, listener: PaseoExtensionListener) => void; events: { on: () => void }; registerCommand: () => void; + registerMcpServer: (name: string, config: unknown) => void; }) => void; }; extension.default({ on: (event, listener) => listeners.set(event, listener), events: { on: () => undefined }, registerCommand: () => undefined, + registerMcpServer, }); return listeners; } +async function readRegisteredMcpServers(extensionPath: string): Promise> { + const servers: Record = {}; + await loadPaseoExtensionListeners(extensionPath, (name, config) => { + servers[name] = config; + }); + return servers; +} + async function applyPaseoExtensionSystemPrompt( extensionPath: string, systemPrompt: string, @@ -3129,6 +3140,102 @@ describe("PiRpcAgentClient", () => { ).rejects.toThrow(`Failed to parse Pi MCP config: ${configPath}`); }); + test("registers MCP servers through Pi's built-in MCP when pi-mcp-adapter is not loaded", async () => { + const pi = new FakePi(); + pi.queueCommands([ + { + name: "mcp", + source: "extension", + sourceInfo: { path: "builtin:mcp", source: "builtin" }, + }, + ]); + const session = await createClient(pi).createSession( + createConfig({ + mcpServers: { + paseo: { + type: "http", + url: "http://127.0.0.1:6767/mcp/agents?callerAgentId=agent-1", + headers: { Authorization: "Bearer token" }, + }, + local: { type: "stdio", command: "node", args: ["server.js"], env: { KEY: "`${x}`" } }, + }, + }), + ); + + expect(session.capabilities.supportsMcpServers).toBe(true); + const actualLaunch = pi.recordedLaunches[1]!; + expect(actualLaunch.argv).not.toContain("--mcp-config"); + const extensionPath = actualLaunch.extensionPaths[0]!; + expect(await readRegisteredMcpServers(extensionPath)).toEqual({ + paseo: { + url: "http://127.0.0.1:6767/mcp/agents?callerAgentId=agent-1", + headers: { Authorization: "Bearer token" }, + }, + local: { command: "node", args: ["server.js"], env: { KEY: "`${x}`" } }, + }); + + await session.close(); + expect(existsSync(extensionPath)).toBe(false); + }); + + test("registers the resumed session's MCP servers through Pi's built-in MCP", async () => { + const pi = new FakePi(); + const client = createClient(pi); + const original = await client.createSession(createConfig()); + const persistence = original.describePersistence()!; + await original.close(); + + pi.queueCommands([{ name: "mcp", source: "extension", sourceInfo: { path: "builtin:mcp" } }]); + const session = await client.resumeSession(persistence, { + mcpServers: { + paseo: { type: "http", url: "http://127.0.0.1:7777/mcp/agents?callerAgentId=agent-2" }, + }, + }); + onTestFinished(() => session.close()); + + expect(session.capabilities.supportsMcpServers).toBe(true); + const actualLaunch = pi.recordedLaunches.at(-1)!; + expect(actualLaunch.session).toBe(persistence.nativeHandle); + expect(actualLaunch.argv).not.toContain("--mcp-config"); + expect(await readRegisteredMcpServers(actualLaunch.extensionPaths[0]!)).toEqual({ + paseo: { url: "http://127.0.0.1:7777/mcp/agents?callerAgentId=agent-2" }, + }); + }); + + test("uses pi-mcp-adapter when it replaces Pi's built-in MCP", async () => { + const agentDir = mkdtempSync(path.join(tmpdir(), "paseo-pi-agent-")); + onTestFinished(() => rmSync(agentDir, { recursive: true, force: true })); + const pi = new FakePi(); + pi.queueCommands([ + { name: "mcp", source: "extension", sourceInfo: { path: "builtin:mcp" } }, + { name: "mcp:1", source: "extension", sourceInfo: { source: "npm:pi-mcp-adapter" } }, + ]); + const session = await createClient(pi).createSession( + createConfig({ + mcpServers: { paseo: { type: "http", url: "http://127.0.0.1:6767/mcp/agents" } }, + }), + { env: { PI_CODING_AGENT_DIR: agentDir } }, + ); + onTestFinished(() => session.close()); + + const actualLaunch = pi.recordedLaunches[1]!; + expect(actualLaunch.argv).toContain("--mcp-config"); + expect(await readRegisteredMcpServers(actualLaunch.extensionPaths[0]!)).toEqual({}); + }); + + test.each([ + { name: "legacy", config: { type: "sse" as const, url: "https://example.com/sse" } }, + { name: "foo.bar", config: { type: "http" as const, url: "https://example.com/mcp" } }, + ])("rejects $name, which Pi's built-in MCP cannot register", async ({ name, config }) => { + const pi = new FakePi(); + pi.queueCommands([{ name: "mcp", source: "extension", sourceInfo: { path: "builtin:mcp" } }]); + + await expect( + createClient(pi).createSession(createConfig({ mcpServers: { [name]: config } })), + ).rejects.toThrow(name); + expect(pi.recordedLaunches).toHaveLength(1); + }); + test("does not pass MCP config when pi-mcp-adapter is not loaded", async () => { const pi = new FakePi(); pi.queueCommands([]); diff --git a/packages/server/src/server/agent/providers/pi/agent.ts b/packages/server/src/server/agent/providers/pi/agent.ts index 9a37c9adc7..1d8b969030 100644 --- a/packages/server/src/server/agent/providers/pi/agent.ts +++ b/packages/server/src/server/agent/providers/pi/agent.ts @@ -262,13 +262,17 @@ interface PiMcpServerConfig { env?: Record; url?: string; headers?: Record; - auth?: false; - oauth?: false; } -interface PiMcpConfigFile { - path: string; - cleanup: () => void; +// Pi's built-in MCP extension takes servers from Paseo's extension; pi-mcp-adapter replaces it +// and takes them from a --mcp-config file. +type PiMcpInjection = + | { kind: "builtin"; servers: Record } + | { kind: "adapter"; configFile: PiTempFile }; + +interface PiPaseoExtensionOptions { + systemPrompt?: string; + mcpServers?: Record; } interface PiTempFile { @@ -487,7 +491,7 @@ function buildResumeStartInput(input: { resumeConfig: PiResumeConfig; sessionFile: string; launchContext: AgentLaunchContext | undefined; - mcpConfig: PiMcpConfigFile | null; + mcpConfigFile: PiTempFile | null; paseoExtension: PiTempFile | null; }): PiStartSessionInput { return { @@ -496,7 +500,7 @@ function buildResumeStartInput(input: { session: input.sessionFile, model: input.resumeConfig.model, thinkingOptionId: normalizePiThinkingOption(input.resumeConfig.thinkingOptionId) ?? undefined, - mcpConfigPath: input.mcpConfig?.path, + mcpConfigPath: input.mcpConfigFile?.path, extensionPaths: input.paseoExtension ? [input.paseoExtension.path] : undefined, }; } @@ -513,11 +517,36 @@ function toPiMcpConfig(config: McpServerConfig): PiMcpServerConfig { return { url: config.url, ...(config.headers ? { headers: config.headers } : {}), - auth: false, - oauth: false, }; } +function toPiMcpAdapterConfig( + config: McpServerConfig, +): PiMcpServerConfig & { auth?: false; oauth?: false } { + const piConfig = toPiMcpConfig(config); + return config.type === "stdio" ? piConfig : { ...piConfig, auth: false, oauth: false }; +} + +function toPiBuiltinMcpServers( + servers: Record, +): Record { + const piServers: Record = {}; + for (const [name, config] of Object.entries(servers)) { + if (!/^[A-Za-z0-9_-]+$/.test(name)) { + throw new Error( + `Pi's built-in MCP cannot register server "${name}": names may only contain letters, digits, "_", and "-".`, + ); + } + if (config.type === "sse") { + throw new Error( + `Pi's built-in MCP cannot register SSE server "${name}": use streamable HTTP or load pi-mcp-adapter.`, + ); + } + piServers[name] = toPiMcpConfig(config); + } + return piServers; +} + function resolvePiAgentDir(env: Record | undefined): string { const configured = env?.PI_CODING_AGENT_DIR?.trim() || process.env.PI_CODING_AGENT_DIR?.trim(); if (!configured) { @@ -558,7 +587,7 @@ function createPiMcpConfigFile( options?: { piGlobalConfigEnv?: Record; }, -): PiMcpConfigFile { +): PiTempFile { const globalConfig = options?.piGlobalConfigEnv ? readPiGlobalMcpConfig(options.piGlobalConfigEnv) : {}; @@ -570,7 +599,7 @@ function createPiMcpConfigFile( } const mcpServers: Record = { ...configuredServers }; for (const [name, serverConfig] of Object.entries(servers)) { - mcpServers[name] = toPiMcpConfig(serverConfig); + mcpServers[name] = toPiMcpAdapterConfig(serverConfig); } const dir = mkdtempSync(join(tmpdir(), "paseo-pi-mcp-")); @@ -587,7 +616,10 @@ function createPiMcpConfigFile( }; } -function createPiPaseoExtensionFile(systemPrompt?: string): PiTempFile { +function createPiPaseoExtensionFile({ + systemPrompt, + mcpServers = {}, +}: PiPaseoExtensionOptions): PiTempFile { const dir = mkdtempSync(join(tmpdir(), "paseo-pi-extension-")); const filePath = join(dir, "paseo-integration.mjs"); writeFileSync( @@ -647,6 +679,9 @@ function createPiPaseoExtensionFile(systemPrompt?: string): PiTempFile { } export default function paseoIntegration(pi) { + for (const [name, config] of Object.entries(${JSON.stringify(mcpServers)})) { + pi.registerMcpServer(name, config); + } const submittedUserMessages = []; ${piExtensionRuntimeBridge} @@ -728,7 +763,8 @@ function createPiPaseoExtensionFile(systemPrompt?: string): PiTempFile { }); } `.trimStart(), - "utf8", + // MCP server headers and env can carry credentials. + { encoding: "utf8", mode: 0o600 }, ); return { path: filePath, @@ -758,6 +794,14 @@ function isPiMcpAdapterCommand(command: PiRpcSlashCommand): boolean { return JSON.stringify(command.sourceInfo).includes("pi-mcp-adapter"); } +function isPiBuiltinMcpCommand(command: PiRpcSlashCommand): boolean { + return ( + command.source === "extension" && + command.name === "mcp" && + command.sourceInfo?.path === "builtin:mcp" + ); +} + function withPiCapabilities(supportsMcpServers: boolean): AgentCapabilityFlags { return { ...PI_CAPABILITIES, @@ -2445,10 +2489,12 @@ export class PiRpcAgentClient implements AgentClient { ...this.runtimeSettings?.env, ...launchContext?.env, }; - const mcpConfig = await this.prepareMcpConfig(config.cwd, config.mcpServers, mcpEnv); - const paseoExtension = createPiPaseoExtensionFile( - composeSystemPromptParts(config.systemPrompt, config.daemonAppendSystemPrompt), - ); + const mcp = await this.prepareMcpInjection(config.cwd, config.mcpServers, mcpEnv); + const mcpConfigFile = mcp?.kind === "adapter" ? mcp.configFile : null; + const paseoExtension = createPiPaseoExtensionFile({ + systemPrompt: composeSystemPromptParts(config.systemPrompt, config.daemonAppendSystemPrompt), + mcpServers: mcp?.kind === "builtin" ? mcp.servers : undefined, + }); let runtimeSession: PiRuntimeSession; try { runtimeSession = await this.runtime.startSession({ @@ -2457,11 +2503,11 @@ export class PiRpcAgentClient implements AgentClient { thinkingOptionId: normalizePiThinkingOption(config.thinkingOptionId) ?? undefined, noSession: config.internal === true, env: launchContext?.env, - mcpConfigPath: mcpConfig?.path, + mcpConfigPath: mcpConfigFile?.path, extensionPaths: paseoExtension ? [paseoExtension.path] : undefined, }); } catch (error) { - mcpConfig?.cleanup(); + mcpConfigFile?.cleanup(); paseoExtension?.cleanup(); throw error; } @@ -2470,15 +2516,15 @@ export class PiRpcAgentClient implements AgentClient { runtimeSession, config, initialState: await runtimeSession.getState(), - capabilities: capabilitiesForSession(mcpConfig !== null), - cleanup: combineCleanup([mcpConfig?.cleanup, paseoExtension?.cleanup]), + capabilities: capabilitiesForSession(mcp !== null), + cleanup: combineCleanup([mcpConfigFile?.cleanup, paseoExtension?.cleanup]), extensionTimeoutMs: this.providerParams.extensionTimeoutMs, logger: this.logger, usagePollScheduler: this.usagePollScheduler, }); } catch (error) { await runtimeSession.close().catch(() => undefined); - mcpConfig?.cleanup(); + mcpConfigFile?.cleanup(); paseoExtension?.cleanup(); throw error; } @@ -2501,17 +2547,19 @@ export class PiRpcAgentClient implements AgentClient { ...this.runtimeSettings?.env, ...launchContext?.env, }; - const mcpConfig = await this.prepareMcpConfig( + const mcp = await this.prepareMcpInjection( resumeConfig.cwd, resumeConfig.config.mcpServers, mcpEnv, ); - const paseoExtension = createPiPaseoExtensionFile( - composeSystemPromptParts( + const mcpConfigFile = mcp?.kind === "adapter" ? mcp.configFile : null; + const paseoExtension = createPiPaseoExtensionFile({ + systemPrompt: composeSystemPromptParts( resumeConfig.config.systemPrompt, resumeConfig.config.daemonAppendSystemPrompt, ), - ); + mcpServers: mcp?.kind === "builtin" ? mcp.servers : undefined, + }); let runtimeSession: PiRuntimeSession; try { runtimeSession = await this.runtime.startSession( @@ -2519,12 +2567,12 @@ export class PiRpcAgentClient implements AgentClient { resumeConfig, sessionFile, launchContext, - mcpConfig, + mcpConfigFile, paseoExtension, }), ); } catch (error) { - mcpConfig?.cleanup(); + mcpConfigFile?.cleanup(); paseoExtension?.cleanup(); throw error; } @@ -2533,15 +2581,15 @@ export class PiRpcAgentClient implements AgentClient { runtimeSession, config: resumeConfig.config, initialState: await runtimeSession.getState(), - capabilities: capabilitiesForSession(mcpConfig !== null), - cleanup: combineCleanup([mcpConfig?.cleanup, paseoExtension?.cleanup]), + capabilities: capabilitiesForSession(mcp !== null), + cleanup: combineCleanup([mcpConfigFile?.cleanup, paseoExtension?.cleanup]), extensionTimeoutMs: this.providerParams.extensionTimeoutMs, logger: this.logger, usagePollScheduler: this.usagePollScheduler, }); } catch (error) { await runtimeSession.close().catch(() => undefined); - mcpConfig?.cleanup(); + mcpConfigFile?.cleanup(); paseoExtension?.cleanup(); throw error; } @@ -2656,33 +2704,47 @@ export class PiRpcAgentClient implements AgentClient { } } - private async prepareMcpConfig( + private async prepareMcpInjection( cwd: string, servers: Record | undefined, env: Record | undefined, - ): Promise { + ): Promise { if (!servers || Object.keys(servers).length === 0) { return null; } - if (!(await this.detectMcpAdapter(cwd, env))) { - return null; + const mcpSupport = await this.detectMcpSupport(cwd, env); + if (mcpSupport === "builtin") { + return { kind: "builtin", servers: toPiBuiltinMcpServers(servers) }; } - return createPiMcpConfigFile(servers, { piGlobalConfigEnv: env }); + if (mcpSupport === "adapter") { + return { + kind: "adapter", + configFile: createPiMcpConfigFile(servers, { piGlobalConfigEnv: env }), + }; + } + return null; } - private async detectMcpAdapter(cwd: string, env?: Record): Promise { + private async detectMcpSupport( + cwd: string, + env?: Record, + ): Promise { const runtimeSession = await this.runtime.startSession({ cwd, env }).catch((error) => { - this.logger.debug({ err: error, cwd }, "Pi MCP adapter probe failed to start"); + this.logger.debug({ err: error, cwd }, "Pi MCP probe failed to start"); return null; }); if (!runtimeSession) { - return false; + return null; } try { - return (await runtimeSession.getCommands()).some(isPiMcpAdapterCommand); + const commands = await runtimeSession.getCommands(); + if (commands.some(isPiMcpAdapterCommand)) { + return "adapter"; + } + return commands.some(isPiBuiltinMcpCommand) ? "builtin" : null; } catch (error) { - this.logger.debug({ err: error, cwd }, "Pi MCP adapter probe failed"); - return false; + this.logger.debug({ err: error, cwd }, "Pi MCP probe failed"); + return null; } finally { await runtimeSession.close().catch(() => undefined); } From d070db15b3eb710b84dd001c29cbbdc8266e675a Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Wed, 30 Sep 2026 18:19:05 +0200 Subject: [PATCH 025/100] fix(acp): report the agent's error when session setup is rejected (#5765) ACP agents reject mode, model, thinking, and feature writes as plain JSON-RPC error objects. Those calls skipped the request error normalization, so schedule runs and metadata generation recorded "[object Object]" instead of the agent's message. Refs #4666 Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> --- .../src/server/agent/providers/acp-agent.ts | 88 ++++++++----- .../generic-acp-agent.request-errors.test.ts | 121 ++++++++++++++++++ 2 files changed, 177 insertions(+), 32 deletions(-) create mode 100644 packages/server/src/server/agent/providers/generic-acp-agent.request-errors.test.ts diff --git a/packages/server/src/server/agent/providers/acp-agent.ts b/packages/server/src/server/agent/providers/acp-agent.ts index 2d47bb3b6b..b097f8b1e5 100644 --- a/packages/server/src/server/agent/providers/acp-agent.ts +++ b/packages/server/src/server/agent/providers/acp-agent.ts @@ -200,15 +200,22 @@ export function summarizeACPRequestError(error: unknown): { return { message: String(error) }; } +// Keeps the JSON-RPC code so callers can still tell invalid params from other failures. +class ACPRequestError extends Error { + readonly code: number; + + constructor(error: ACPError) { + super(summarizeACPRequestError(error).message, { cause: error }); + this.name = "ACPRequestError"; + this.code = error.code; + } +} + function toACPRequestError(error: unknown): Error { if (!isACPError(error)) { return error instanceof Error ? error : new Error(String(error)); } - - const summary = summarizeACPRequestError(error); - const next = new Error(summary.message); - next.name = "ACPRequestError"; - return next; + return new ACPRequestError(error); } function resolveTerminalCommand( @@ -2021,8 +2028,9 @@ export class ACPAgentSession implements AgentSession, ACPClient { } const context = this.createProviderModeWriterContext(modeId, selection); - const providerResult = this.providerModeWriter - ? await this.providerModeWriter(context) + const providerModeWriter = this.providerModeWriter; + const providerResult = providerModeWriter + ? await this.runACPRequest(() => providerModeWriter(context)) : { handled: false }; if (providerResult.handled) { this.currentMode = providerResult.currentModeId ?? modeId; @@ -2067,15 +2075,17 @@ export class ACPAgentSession implements AgentSession, ACPClient { } } - if (this.beforeModeWriter) { - const beforeResult = await this.beforeModeWriter(context); + const beforeModeWriter = this.beforeModeWriter; + if (beforeModeWriter) { + const beforeResult = await this.runACPRequest(() => beforeModeWriter(context)); if (beforeResult?.configOptions) { this.configOptions = this.transformConfigOptions(beforeResult.configOptions); } } if (selection.hasAvailableModes) { - await this.connection.setSessionMode({ sessionId: this.sessionId, modeId }); + const { connection, sessionId } = this; + await this.runACPRequest(() => connection.setSessionMode({ sessionId, modeId })); this.currentMode = modeId; this.pushEvent({ type: "mode_changed", @@ -2091,11 +2101,14 @@ export class ACPAgentSession implements AgentSession, ACPClient { throw new Error(`${this.provider} does not expose ACP mode switching`); } - const response = await this.connection.setSessionConfigOption({ - sessionId: this.sessionId, - configId: modeOption.id, - value: modeId, - }); + const { connection, sessionId } = this; + const response = await this.runACPRequest(() => + connection.setSessionConfigOption({ + sessionId, + configId: modeOption.id, + value: modeId, + }), + ); this.currentMode = this.applyConfigOptionResponse({ response, configId: modeOption.id, @@ -2206,11 +2219,14 @@ export class ACPAgentSession implements AgentSession, ACPClient { return; } - const response = await this.connection.setSessionConfigOption({ - sessionId: this.sessionId, - configId: modelOption.id, - value: modelId, - }); + const { connection, sessionId } = this; + const response = await this.runACPRequest(() => + connection.setSessionConfigOption({ + sessionId, + configId: modelOption.id, + value: modelId, + }), + ); this.currentModel = this.applyConfigOptionResponse({ response, configId: modelOption.id, @@ -2234,8 +2250,10 @@ export class ACPAgentSession implements AgentSession, ACPClient { return; } - if (this.thinkingOptionWriter) { - await this.thinkingOptionWriter(this.connection, this.sessionId, thinkingOptionId); + const thinkingOptionWriter = this.thinkingOptionWriter; + if (thinkingOptionWriter) { + const { connection, sessionId } = this; + await this.runACPRequest(() => thinkingOptionWriter(connection, sessionId, thinkingOptionId)); this.thinkingOptionId = thinkingOptionId; this.pushEvent({ type: "thinking_option_changed", @@ -2252,11 +2270,14 @@ export class ACPAgentSession implements AgentSession, ACPClient { if (!option) { throw new Error(`${this.provider} does not expose ACP thought-level selection`); } - const response = await this.connection.setSessionConfigOption({ - sessionId: this.sessionId, - configId: option.id, - value: thinkingOptionId, - }); + const { connection, sessionId } = this; + const response = await this.runACPRequest(() => + connection.setSessionConfigOption({ + sessionId, + configId: option.id, + value: thinkingOptionId, + }), + ); this.thinkingOptionId = this.applyConfigOptionResponse({ response, configId: option.id, @@ -2302,11 +2323,14 @@ export class ACPAgentSession implements AgentSession, ACPClient { ); } - const response = await this.connection.setSessionConfigOption({ - sessionId: this.sessionId, - configId: option.id, - value: requestedValue, - }); + const { connection, sessionId } = this; + const response = await this.runACPRequest(() => + connection.setSessionConfigOption({ + sessionId, + configId: option.id, + value: requestedValue, + }), + ); const currentValue = this.applyConfigOptionResponse({ response, configId: option.id, diff --git a/packages/server/src/server/agent/providers/generic-acp-agent.request-errors.test.ts b/packages/server/src/server/agent/providers/generic-acp-agent.request-errors.test.ts new file mode 100644 index 0000000000..9e652a37d5 --- /dev/null +++ b/packages/server/src/server/agent/providers/generic-acp-agent.request-errors.test.ts @@ -0,0 +1,121 @@ +import { mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; + +import { describe, expect, test } from "vitest"; + +import { createTestLogger } from "../../../test-utils/test-logger.js"; +import { GenericACPAgentClient } from "./generic-acp-agent.js"; + +describe("GenericACPAgentClient request errors", () => { + test("reports the agent's message when it rejects the configured mode", async () => { + await withFakeACPAgent("reject-set-mode", async (command, cwd) => { + const client = new GenericACPAgentClient({ logger: createTestLogger(), command }); + + const created = client.createSession({ provider: "acp", cwd, modeId: "plan" }); + + await expect(created).rejects.toBeInstanceOf(Error); + await expect(created).rejects.toThrow("Internal error: Mode plan is not allowed"); + }); + }); + + test("reports the agent's message when it rejects the configured thinking option", async () => { + await withFakeACPAgent("reject-set-config-option", async (command, cwd) => { + const client = new GenericACPAgentClient({ logger: createTestLogger(), command }); + + const created = client.createSession({ provider: "acp", cwd, thinkingOptionId: "high" }); + + await expect(created).rejects.toBeInstanceOf(Error); + await expect(created).rejects.toThrow("Internal error: Effort high is not allowed"); + }); + }); +}); + +async function withFakeACPAgent( + mode: "reject-set-mode" | "reject-set-config-option", + run: (command: [string, ...string[]], cwd: string) => Promise, +): Promise { + const testDir = await mkdtemp(path.join(tmpdir(), "paseo-acp-request-errors-")); + try { + const scriptPath = path.join(testDir, "fake-acp-agent.cjs"); + await writeFile(scriptPath, fakeACPAgentScript, "utf8"); + await run([process.execPath, scriptPath, mode], testDir); + } finally { + await rm(testDir, { recursive: true, force: true }); + } +} + +const fakeACPAgentScript = ` +const readline = require("node:readline"); + +const mode = process.argv[2]; +const rl = readline.createInterface({ input: process.stdin }); + +function write(message) { + process.stdout.write(JSON.stringify({ jsonrpc: "2.0", ...message }) + "\\n"); +} + +rl.on("line", (line) => { + const message = JSON.parse(line); + if (message.method === "initialize") { + write({ + id: message.id, + result: { + protocolVersion: message.params?.protocolVersion ?? 1, + agentCapabilities: { sessionCapabilities: { close: {} } }, + }, + }); + return; + } + + if (message.method === "session/new") { + write({ + id: message.id, + result: { + sessionId: "session-1", + modes: { + currentModeId: "agent", + availableModes: [ + { id: "agent", name: "Agent" }, + { id: "plan", name: "Plan" }, + ], + }, + configOptions: [ + { + id: "effort", + name: "Effort", + category: "thought_level", + type: "select", + currentValue: "low", + options: [ + { value: "low", name: "Low" }, + { value: "high", name: "High" }, + ], + }, + ], + }, + }); + return; + } + + if (message.method === "session/set_mode" && mode === "reject-set-mode") { + write({ + id: message.id, + error: { code: -32603, message: "Internal error", data: { details: "Mode plan is not allowed" } }, + }); + return; + } + + if (message.method === "session/set_config_option" && mode === "reject-set-config-option") { + write({ + id: message.id, + error: { code: -32603, message: "Internal error", data: { details: "Effort high is not allowed" } }, + }); + return; + } + + if (message.id !== undefined) { + write({ id: message.id, result: {} }); + } +}); +`; From 364e6456e0257cfe5c19a07e5a48d2aa4c3fd2a0 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Wed, 30 Sep 2026 18:11:23 +0100 Subject: [PATCH 026/100] Add confirmation for new hosts (#5753) * Add confirmation for new hosts Opening a pairing link (an `#offer=` URL, a scanned QR code, or a pasted link) now asks the user to confirm before the host is added or connected. The prompt shows the host id, its key fingerprint, and the relay. Cancel leaves no new host and no connection. The host runtime owns the check: `upsertConnectionFromOfferUrl` and `probeAndUpsertConnectionFromOfferUrl` ask a registered confirmer before doing anything. `HostTrustPromptModal`, mounted at the app root, registers the confirmer and renders the prompt with the shared confirm sheet. Without a registered confirmer, pairing links are declined. Adds a test that an offer link with the confirmation declined adds no host and attempts no connection. * Only ask for new hosts or changed connections A pairing link now connects without the confirmation when it matches a saved host exactly (same server id, key and relay). It still asks for a new host, and for a saved host whose key or relay differs from what is saved, with the "This link changes how you connect to this host" text. After the user approves a link that then needs a password, the password retry no longer asks again. The approval is an `OfferApproval` owned by one pairing flow (one QR scan, or one open pair-link or add-host modal) and only covers the exact offer that was approved. Adds tests for a matching saved host, a changed key or relay, and the password retry. * Move host confirmation into the host runtime The host runtime now owns the pending host confirmation and the decision to ask. The sheet reads the pending confirmation and sends the answer; nothing registers into a module variable. A pairing flow is a LinkPairing the runtime hands out, so the password retry for a confirmed link doesn't ask again without views creating or resetting approval objects. Cancel is a result instead of an error. Pairing waits for saved hosts to load before deciding whether to ask. * Stop QR scanning after an attempt ends The scan screen now keeps its workflow in a small model. When the user cancels the host confirmation, closes the password prompt, or the attempt fails, scanning stops until they tap Scan QR code again. Before, the code still in front of the camera started a new attempt on the next frame. A host confirmation answer is now tied to the request it answers, so a tap on one sheet never answers a newer one. Relay hosts can no longer be added through HostMutations without the confirmation, the add-host modal drops the reset its per-open mount made redundant, and the fingerprint comment says what the shortened form shows. --- .../app/e2e/browser/host-confirmation.spec.ts | 51 +++ .../e2e/support/helpers/host-confirmation.ts | 95 ++++ packages/app/src/app/_layout.tsx | 2 + packages/app/src/app/pair-scan.tsx | 85 ++-- .../app/src/components/add-host-modal.tsx | 46 +- .../app/src/components/pair-link-modal.tsx | 41 +- .../app/src/hosts/host-confirmation-sheet.tsx | 109 +++++ .../app/src/hosts/pair-scan-model.test.ts | 116 +++++ packages/app/src/hosts/pair-scan-model.ts | 103 +++++ packages/app/src/i18n/resources/ar.ts | 11 + packages/app/src/i18n/resources/en.ts | 11 + packages/app/src/i18n/resources/es.ts | 11 + packages/app/src/i18n/resources/fr.ts | 11 + packages/app/src/i18n/resources/ja.ts | 11 + packages/app/src/i18n/resources/ko.ts | 11 + packages/app/src/i18n/resources/pt-BR.ts | 11 + packages/app/src/i18n/resources/ru.ts | 11 + packages/app/src/i18n/resources/zh-CN.ts | 10 + packages/app/src/runtime/host-confirmation.ts | 97 ++++ packages/app/src/runtime/host-runtime.test.ts | 431 +++++++++++++----- packages/app/src/runtime/host-runtime.ts | 214 ++++++--- packages/app/src/types/host-connection.ts | 15 + 22 files changed, 1224 insertions(+), 279 deletions(-) create mode 100644 packages/app/e2e/browser/host-confirmation.spec.ts create mode 100644 packages/app/e2e/support/helpers/host-confirmation.ts create mode 100644 packages/app/src/hosts/host-confirmation-sheet.tsx create mode 100644 packages/app/src/hosts/pair-scan-model.test.ts create mode 100644 packages/app/src/hosts/pair-scan-model.ts create mode 100644 packages/app/src/runtime/host-confirmation.ts diff --git a/packages/app/e2e/browser/host-confirmation.spec.ts b/packages/app/e2e/browser/host-confirmation.spec.ts new file mode 100644 index 0000000000..6de8da14ce --- /dev/null +++ b/packages/app/e2e/browser/host-confirmation.spec.ts @@ -0,0 +1,51 @@ +import { test, expect } from "../support/fixtures"; +import { openSettings } from "../support/helpers/app"; +import { expectAppRoute } from "../support/helpers/route-assertions"; +import { + buildPairingLink, + cancelHostConfirmation, + connectToConfirmedHost, + expectHostConfirmationFor, + expectHostInHostPicker, + hostConfirmation, + openPairingLink, + saveLinkedHostBeforeLoad, + type LinkedHost, +} from "../support/helpers/host-confirmation"; + +// An unreachable relay: the link saves the host without connecting first. +const linkedHost: LinkedHost = { + serverId: "srv_link_confirmation", + relayEndpoint: "127.0.0.1:59998", + daemonPublicKeyB64: "bGlua0NvbmZpcm1hdGlvbktleQ", +}; + +test("a pairing link for a new host asks before saving it", async ({ page }) => { + const link = buildPairingLink(linkedHost); + + await test.step("Cancel saves nothing and stays on the page", async () => { + await openPairingLink(page, link); + await expectHostConfirmationFor(page, linkedHost); + await cancelHostConfirmation(page); + await expectAppRoute(page, "/settings/general"); + await expectHostInHostPicker(page, linkedHost.serverId, "not listed"); + }); + + await test.step("Connect saves the host and opens a project", async () => { + await openPairingLink(page, link); + await expectHostConfirmationFor(page, linkedHost); + await connectToConfirmedHost(page); + await expectAppRoute(page, "/open-project"); + await openSettings(page); + await expectHostInHostPicker(page, linkedHost.serverId, "listed"); + }); +}); + +test("a pairing link for a saved host connects without asking", async ({ page }) => { + await saveLinkedHostBeforeLoad(page, linkedHost); + + await openPairingLink(page, buildPairingLink(linkedHost)); + + await expectAppRoute(page, "/open-project", { timeout: 30_000 }); + await expect(hostConfirmation(page)).toHaveCount(0); +}); diff --git a/packages/app/e2e/support/helpers/host-confirmation.ts b/packages/app/e2e/support/helpers/host-confirmation.ts new file mode 100644 index 0000000000..f6de576fc0 --- /dev/null +++ b/packages/app/e2e/support/helpers/host-confirmation.ts @@ -0,0 +1,95 @@ +import { expect, type Locator, type Page } from "@playwright/test"; + +const EXTRA_HOSTS_KEY = "@paseo:e2e-extra-hosts"; + +export interface LinkedHost { + serverId: string; + relayEndpoint: string; + daemonPublicKeyB64: string; +} + +/** + * A pairing link that opens on the settings page. `/` redirects to + * `/open-project` on its own, so starting in settings leaves the pairing link + * as the only thing that can move the app to `/open-project`. + */ +export function buildPairingLink(host: LinkedHost): string { + const offer = { + v: 2, + serverId: host.serverId, + daemonPublicKeyB64: host.daemonPublicKeyB64, + relay: { endpoint: host.relayEndpoint, useTls: false }, + }; + const encoded = Buffer.from(JSON.stringify(offer), "utf8").toString("base64url"); + return `/settings/general#offer=${encoded}`; +} + +/** Loads the app from a pairing link, the way opening the link in a browser does. */ +export async function openPairingLink(page: Page, link: string): Promise { + // Leave the link's page first so the link always loads the app from scratch. + await page.goto("/"); + await page.goto(link); +} + +/** Saves a relay host before the app loads, as if it had been paired in an earlier session. */ +export async function saveLinkedHostBeforeLoad(page: Page, host: LinkedHost): Promise { + await page.goto("/"); + const nowIso = new Date().toISOString(); + const connectionId = `relay:${host.relayEndpoint}`; + const savedHost = { + serverId: host.serverId, + label: host.serverId, + connections: [ + { + id: connectionId, + type: "relay", + relayEndpoint: host.relayEndpoint, + useTls: false, + daemonPublicKeyB64: host.daemonPublicKeyB64, + }, + ], + preferredConnectionId: connectionId, + createdAt: nowIso, + updatedAt: nowIso, + }; + await page.evaluate(({ key, hosts }) => localStorage.setItem(key, JSON.stringify(hosts)), { + key: EXTRA_HOSTS_KEY, + hosts: [savedHost], + }); +} + +export function hostConfirmation(page: Page): Locator { + return page.getByTestId("host-confirmation"); +} + +export async function expectHostConfirmationFor(page: Page, host: LinkedHost): Promise { + const sheet = hostConfirmation(page); + await expect(sheet.getByText("Connect to this host?", { exact: true })).toBeVisible({ + timeout: 30_000, + }); + await expect(sheet.getByTestId("host-confirmation-server-id")).toHaveText(host.serverId); + await expect(sheet.getByTestId("host-confirmation-relay")).toHaveText(host.relayEndpoint); +} + +export async function cancelHostConfirmation(page: Page): Promise { + await hostConfirmation(page).getByRole("button", { name: "Cancel", exact: true }).click(); + await expect(hostConfirmation(page)).toHaveCount(0); +} + +export async function connectToConfirmedHost(page: Page): Promise { + await hostConfirmation(page).getByRole("button", { name: "Connect", exact: true }).click(); + await expect(hostConfirmation(page)).toHaveCount(0); +} + +/** Checks the host picker on the settings page the app is already showing. */ +export async function expectHostInHostPicker( + page: Page, + serverId: string, + expected: "listed" | "not listed", +): Promise { + await page.getByTestId("settings-host-picker").click(); + await expect(page.getByTestId(`settings-host-picker-item-${serverId}`)).toHaveCount( + expected === "listed" ? 1 : 0, + ); + await page.keyboard.press("Escape"); +} diff --git a/packages/app/src/app/_layout.tsx b/packages/app/src/app/_layout.tsx index cf682e7611..0c926c559c 100644 --- a/packages/app/src/app/_layout.tsx +++ b/packages/app/src/app/_layout.tsx @@ -45,6 +45,7 @@ import { WorkspaceSetupDialog } from "@/components/workspace-setup-dialog"; import { WorkspaceShortcutTargetsSubscriber } from "@/components/workspace-shortcut-targets-subscriber"; import { FloatingPanelPortalHost } from "@/components/ui/floating-panel-portal"; import { HostChooserModal, useHostChooser } from "@/hosts/host-chooser"; +import { HostConfirmationSheet } from "@/hosts/host-confirmation-sheet"; import { getIsElectronRuntime, HEADER_INNER_HEIGHT, @@ -606,6 +607,7 @@ function AppContainer({ children, chromeEnabled: chromeEnabledOverride }: AppCon + diff --git a/packages/app/src/app/pair-scan.tsx b/packages/app/src/app/pair-scan.tsx index aae0073324..6aea5225d6 100644 --- a/packages/app/src/app/pair-scan.tsx +++ b/packages/app/src/app/pair-scan.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useState, useSyncExternalStore } from "react"; import { useTranslation } from "react-i18next"; import { Pressable, Text, View } from "react-native"; import { useLocalSearchParams, useRouter, type Href } from "expo-router"; @@ -7,10 +7,12 @@ import { StyleSheet, useUnistyles } from "react-native-unistyles"; import { CameraView, useCameraPermissions } from "expo-camera"; import type { BarcodeScanningResult, BarcodeSettings } from "expo-camera"; import { getHostRuntimeStore } from "@/runtime/host-runtime"; +import { openPairScan } from "@/hosts/pair-scan-model"; import { buildHostRootRoute, buildSettingsHostRoute } from "@/utils/host-routes"; import { isWeb } from "@/constants/platform"; import { BackHeader } from "@/components/headers/back-header"; import { PairLinkModal } from "@/components/pair-link-modal"; +import { Button } from "@/components/ui/button"; const styles = StyleSheet.create((theme) => ({ container: { @@ -73,6 +75,9 @@ const styles = StyleSheet.create((theme) => ({ borderBottomWidth: 4, borderBottomRightRadius: 12, }, + scanAgainButton: { + marginTop: theme.spacing[6], + }, helperText: { marginTop: theme.spacing[6], color: theme.colors.foregroundMuted, @@ -108,16 +113,6 @@ const styles = StyleSheet.create((theme) => ({ }, })); -function extractOfferUrlFromScan(result: BarcodeScanningResult): string | null { - const raw = typeof result.data === "string" ? result.data.trim() : ""; - if (!raw) return null; - - if (raw.includes("#offer=") || raw.includes("#connect=") || raw.startsWith("relay://")) - return raw; - - return null; -} - export default function PairScanScreen() { const { theme } = useUnistyles(); const { t } = useTranslation(); @@ -129,9 +124,6 @@ export default function PairScanScreen() { const source = typeof params.source === "string" ? params.source : "settings"; const [permission, requestPermission] = useCameraPermissions(); - const [isPairing, setIsPairing] = useState(false); - const [scanError, setScanError] = useState(null); - const [passwordOfferUrl, setPasswordOfferUrl] = useState(null); const navigateToPairedHost = useCallback( (serverId: string) => { @@ -144,6 +136,19 @@ export default function PairScanScreen() { [router, source], ); + const [scan] = useState(() => + openPairScan({ + importConnectionLink: (link) => + getHostRuntimeStore().importConnectionLink( + link, + source === "onboarding" ? "hostRoot" : "hostSettings", + ), + onConnected: navigateToPairedHost, + }), + ); + const scanState = useSyncExternalStore(scan.subscribe, scan.getState, scan.getState); + useEffect(() => () => scan.close(), [scan]); + const closeToSource = useCallback(() => { try { router.back(); @@ -159,30 +164,12 @@ export default function PairScanScreen() { }, [permission, requestPermission]); const handleScan = useCallback( - (result: BarcodeScanningResult) => { - if (isPairing) return; - const offerUrl = extractOfferUrlFromScan(result); - if (!offerUrl) return; - - const store = getHostRuntimeStore(); - if (passwordOfferUrl) return; - setIsPairing(true); - setScanError(null); - void store - .importConnectionLink(offerUrl, source === "onboarding" ? "hostRoot" : "hostSettings") - .then((outcome) => { - if (outcome.status === "connected") navigateToPairedHost(outcome.serverId); - else setPasswordOfferUrl(offerUrl); - return outcome; - }) - .catch((error) => setScanError(error instanceof Error ? error.message : String(error))) - .finally(() => setIsPairing(false)); - }, - [isPairing, navigateToPairedHost, passwordOfferUrl, source], + (result: BarcodeScanningResult) => + scan.scan(typeof result.data === "string" ? result.data : ""), + [scan], ); const handleRouterBack = useCallback(() => router.back(), [router]); - const closePasswordModal = useCallback(() => setPasswordOfferUrl(null), []); const savePasswordPairing = useCallback( ({ serverId }: { serverId: string }) => navigateToPairedHost(serverId), [navigateToPairedHost], @@ -240,24 +227,38 @@ export default function PairScanScreen() { barcodeScannerSettings={BARCODE_SCANNER_SETTINGS} onBarcodeScanned={handleScan} /> - + - {isPairing ? {t("pairing.scan.pairing")} : null} - {scanError ? {scanError} : null} + {scanState.status === "pairing" ? ( + {t("pairing.scan.pairing")} + ) : null} + {scanState.status === "stopped" && scanState.error ? ( + {scanState.error} + ) : null} + {scanState.status === "stopped" ? ( + + ) : null} )} diff --git a/packages/app/src/components/add-host-modal.tsx b/packages/app/src/components/add-host-modal.tsx index b388d76047..bf364f1040 100644 --- a/packages/app/src/components/add-host-modal.tsx +++ b/packages/app/src/components/add-host-modal.tsx @@ -294,11 +294,23 @@ export interface AddHostModalProps { } export function AddHostModal({ visible, onClose, onCancel, onSaved }: AddHostModalProps) { + return ( + + ); +} + +function AddHostModalContent({ visible, onClose, onCancel, onSaved }: AddHostModalProps) { const { theme } = useUnistyles(); const { t } = useTranslation(); const daemons = useHosts(); - const { probeAndUpsertDirectConnection, probeAndUpsertConnectionFromOfferUrl } = - useHostMutations(); + const { probeAndUpsertDirectConnection, beginLinkPairing } = useHostMutations(); + const [linkPairing] = useState(() => beginLinkPairing()); const isMobile = useIsCompactFormFactor(); const [isSaving, setIsSaving] = useState(false); @@ -313,18 +325,6 @@ export function AddHostModal({ visible, onClose, onCancel, onSaved }: AddHostMod const [inputResetKey, bumpInputResetKey] = useReducer((key: number) => key + 1, 0); const advancedTarget = useRef(new PairingTargetTracker("", true)); - const clearInput = useCallback(() => { - setHost(""); - setPort("6767"); - setUseTls(false); - setPassword(""); - setIsPasswordVisible(false); - setIsAdvancedOpen(false); - setAdvancedUri(""); - advancedTarget.current = new PairingTargetTracker("", true); - bumpInputResetKey(); - }, []); - const connectIcon = useMemo( () => , [theme.colors.accentForeground], @@ -358,29 +358,25 @@ export function AddHostModal({ visible, onClose, onCancel, onSaved }: AddHostMod ); const header = useMemo(() => ({ title: t("pairing.direct.title") }), [t]); + // Each open mounts a fresh form (see AddHostModal), so closing needs no reset. const handleClose = useCallback(() => { if (isSaving) return; - clearInput(); - setErrorMessage(""); onClose(); - }, [isSaving, clearInput, onClose]); + }, [isSaving, onClose]); const handleCancel = useCallback(() => { if (isSaving) return; - clearInput(); - setErrorMessage(""); (onCancel ?? onClose)(); - }, [isSaving, clearInput, onCancel, onClose]); + }, [isSaving, onCancel, onClose]); const handleSaveRelay = useCallback( async (relayUri: string) => { try { setIsSaving(true); setErrorMessage(""); - const { profile, serverId, hostname } = await probeAndUpsertConnectionFromOfferUrl( - relayUri, - password || undefined, - ); + const result = await linkPairing.submit(relayUri, password || undefined); + if (result.status === "cancelled") return; + const { profile, serverId, hostname } = result; const isNewHost = !daemons.some((daemon) => daemon.serverId === serverId); onSaved?.({ profile, serverId, hostname, isNewHost }); handleClose(); @@ -396,9 +392,9 @@ export function AddHostModal({ visible, onClose, onCancel, onSaved }: AddHostMod daemons, directConnectionLabels.invalidConnection, handleClose, + linkPairing, onSaved, password, - probeAndUpsertConnectionFromOfferUrl, ], ); diff --git a/packages/app/src/components/pair-link-modal.tsx b/packages/app/src/components/pair-link-modal.tsx index d68f72bedd..8ed9cefba9 100644 --- a/packages/app/src/components/pair-link-modal.tsx +++ b/packages/app/src/components/pair-link-modal.tsx @@ -5,7 +5,7 @@ import { StyleSheet, useUnistyles } from "react-native-unistyles"; import { useIsCompactFormFactor } from "@/constants/layout"; import { Link } from "lucide-react-native"; import type { HostProfile } from "@/types/host-connection"; -import { useHosts, useHostMutations } from "@/runtime/host-runtime"; +import { useHosts, useHostMutations, type PasswordRequiredPairing } from "@/runtime/host-runtime"; import { parseRelayConnectionUri } from "@/utils/daemon-endpoints"; import { parseConnectionOfferFromUrl } from "@getpaseo/protocol/connection-offer"; import { AdaptiveModalSheet, AdaptiveTextInput, type SheetHeader } from "./adaptive-modal-sheet"; @@ -62,8 +62,8 @@ const styles = StyleSheet.create((theme) => ({ export interface PairLinkModalProps { visible: boolean; - initialUrl?: string; - initialPasswordRequired?: boolean; + /** Continues a confirmed pairing whose host asked for a password. */ + passwordRequired?: PasswordRequiredPairing; onClose: () => void; onCancel?: () => void; onSaved?: (result: { @@ -76,18 +76,16 @@ export interface PairLinkModalProps { export function PairLinkModal({ visible, - initialUrl, - initialPasswordRequired = false, + passwordRequired, onClose, onCancel, onSaved, }: PairLinkModalProps) { return ( passwordRequired?.pairing ?? beginLinkPairing()); + const initialUrl = passwordRequired?.link; const isMobile = useIsCompactFormFactor(); const offerUrlRef = useRef(initialUrl ?? ""); @@ -115,7 +114,7 @@ function PairLinkModalContent({ const [isSaving, setIsSaving] = useState(false); const [errorMessage, setErrorMessage] = useState(""); const [password, setPassword] = useState(""); - const [needsPassword, setNeedsPassword] = useState(initialPasswordRequired); + const [needsPassword, setNeedsPassword] = useState(passwordRequired !== undefined); const [passwordResetKey, resetPasswordInput] = useReducer((key: number) => key + 1, 0); const clearInput = useCallback(() => { @@ -162,10 +161,9 @@ function PairLinkModalContent({ try { setIsSaving(true); setErrorMessage(""); - const { profile, serverId, hostname } = await probeAndUpsertConnectionFromOfferUrl( - raw, - password || undefined, - ); + const result = await pairing.submit(raw, password || undefined); + if (result.status === "cancelled") return; + const { profile, serverId, hostname } = result; const isNewHost = !daemons.some((daemon) => daemon.serverId === serverId); onSaved?.({ profile, serverId, hostname, isNewHost }); handleClose(); @@ -184,16 +182,7 @@ function PairLinkModalContent({ setIsSaving(false); } }, - [ - daemons, - handleClose, - isMobile, - isSaving, - onSaved, - password, - t, - probeAndUpsertConnectionFromOfferUrl, - ], + [daemons, handleClose, isMobile, isSaving, onSaved, password, t, pairing], ); const handleChangeOfferUrl = useCallback((next: string) => { diff --git a/packages/app/src/hosts/host-confirmation-sheet.tsx b/packages/app/src/hosts/host-confirmation-sheet.tsx new file mode 100644 index 0000000000..c83be51a30 --- /dev/null +++ b/packages/app/src/hosts/host-confirmation-sheet.tsx @@ -0,0 +1,109 @@ +import { useCallback, useMemo } from "react"; +import { Text, View } from "react-native"; +import { StyleSheet } from "react-native-unistyles"; +import { useTranslation } from "react-i18next"; +import { AdaptiveModalSheet, type SheetHeader } from "@/components/adaptive-modal-sheet"; +import { Button } from "@/components/ui/button"; +import { settingsStyles } from "@/styles/settings"; +import { useHostConfirmation } from "@/runtime/host-runtime"; + +/** Shows the host confirmation the host runtime is waiting on. Mount once at the app root. */ +export function HostConfirmationSheet() { + const { t } = useTranslation(); + const { pending, answer } = useHostConfirmation(); + + const cancel = useCallback(() => answer(false), [answer]); + const connect = useCallback(() => answer(true), [answer]); + const header = useMemo(() => ({ title: t("pairing.hostConfirmation.title") }), [t]); + + if (!pending) return null; + + return ( + + + {pending.kind === "changedConnection" + ? t("pairing.hostConfirmation.descriptionChanged") + : t("pairing.hostConfirmation.description")} + + + + + + + + + + + + ); +} + +function DetailRow({ + label, + value, + showBorder = false, + testID, +}: { + label: string; + value: string; + showBorder?: boolean; + testID?: string; +}) { + return ( + + + {label} + + {value} + + + + ); +} + +// Same shape as the remove-host and remove-connection confirm sheets in +// screens/settings/host-page.tsx. +const styles = StyleSheet.create((theme) => ({ + confirmText: { + color: theme.colors.foregroundMuted, + fontSize: theme.fontSize.base, + }, + confirmActions: { + flexDirection: "row", + alignItems: "center", + gap: theme.spacing[2], + marginTop: theme.spacing[4], + }, +})); + +const FLEX_1_STYLE = { flex: 1 }; diff --git a/packages/app/src/hosts/pair-scan-model.test.ts b/packages/app/src/hosts/pair-scan-model.test.ts new file mode 100644 index 0000000000..0ca69b9d24 --- /dev/null +++ b/packages/app/src/hosts/pair-scan-model.test.ts @@ -0,0 +1,116 @@ +import { describe, expect, it, vi } from "vitest"; +import type { ConnectionLinkImport, LinkPairing } from "@/runtime/host-runtime"; +import { openPairScan, type PairScanModel } from "./pair-scan-model"; + +const LINK = "https://app.paseo.sh/#offer=abc"; +const PAIRING: LinkPairing = { submit: async () => ({ status: "cancelled" }) }; + +/** The host runtime's side of a scan: each import resolves with the next queued outcome. */ +function createScanner(outcomes: Array) { + const imports: string[] = []; + const connected: string[] = []; + const scan = openPairScan({ + importConnectionLink: async (link) => { + imports.push(link); + const outcome = outcomes.shift(); + if (!outcome) throw new Error("No outcome queued for this import"); + if (outcome instanceof Error) throw outcome; + return outcome; + }, + onConnected: (serverId) => connected.push(serverId), + }); + return { scan, imports, connected }; +} + +async function settled(scan: PairScanModel): Promise { + await vi.waitFor(() => expect(scan.getState().status).not.toBe("pairing")); +} + +describe("openPairScan", () => { + it("stops scanning after Cancel until the user scans again", async () => { + const { scan, imports } = createScanner([{ status: "cancelled" }, { status: "cancelled" }]); + + scan.scan(LINK); + await settled(scan); + + // The code is still in front of the camera. + scan.scan(LINK); + scan.scan(LINK); + await Promise.resolve(); + expect(imports).toEqual([LINK]); + expect(scan.getState()).toEqual({ status: "stopped", error: null }); + + scan.scanAgain(); + scan.scan(LINK); + await settled(scan); + expect(imports).toEqual([LINK, LINK]); + }); + + it("starts one attempt per code while pairing, and ignores codes that are not pairing links", async () => { + const { scan, imports, connected } = createScanner([ + { status: "connected", serverId: "srv_scan" }, + ]); + + scan.scan("https://example.com/not-a-pairing-link"); + expect(scan.getState()).toEqual({ status: "scanning" }); + + scan.scan(LINK); + scan.scan(LINK); + await settled(scan); + + expect(imports).toEqual([LINK]); + expect(connected).toEqual(["srv_scan"]); + scan.scan(LINK); + expect(imports).toEqual([LINK]); + }); + + it("ends the attempt when the password prompt closes", async () => { + const { scan, imports } = createScanner([ + { status: "password_required", link: LINK, pairing: PAIRING }, + ]); + + scan.scan(LINK); + await settled(scan); + expect(scan.getState()).toEqual({ + status: "passwordRequired", + passwordRequired: { link: LINK, pairing: PAIRING }, + }); + scan.scan(LINK); + + scan.closePassword(); + scan.scan(LINK); + + expect(scan.getState()).toEqual({ status: "stopped", error: null }); + expect(imports).toEqual([LINK]); + }); + + it("stops with the error when an attempt fails", async () => { + const { scan, imports } = createScanner([new Error("Server disconnected")]); + + scan.scan(LINK); + await settled(scan); + scan.scan(LINK); + + expect(scan.getState()).toEqual({ status: "stopped", error: "Server disconnected" }); + expect(imports).toEqual([LINK]); + }); + + it("does not navigate for an attempt that finishes after the screen closed", async () => { + let finish: (outcome: ConnectionLinkImport) => void = () => {}; + const connected: string[] = []; + const scan = openPairScan({ + importConnectionLink: () => + new Promise((resolve) => { + finish = resolve; + }), + onConnected: (serverId) => connected.push(serverId), + }); + + scan.scan(LINK); + scan.close(); + finish({ status: "connected", serverId: "srv_scan" }); + await Promise.resolve(); + + expect(connected).toEqual([]); + }); +}); diff --git a/packages/app/src/hosts/pair-scan-model.ts b/packages/app/src/hosts/pair-scan-model.ts new file mode 100644 index 0000000000..4dca380543 --- /dev/null +++ b/packages/app/src/hosts/pair-scan-model.ts @@ -0,0 +1,103 @@ +import type { ConnectionLinkImport, PasswordRequiredPairing } from "@/runtime/host-runtime"; + +/** + * One visit to the QR scan screen. The camera reports every frame that shows + * a code, so an attempt that ends (Cancel on the host confirmation, a closed + * password prompt, or an error) stops scanning until the user taps scan again. + * Without that, the code still in front of the camera starts a new attempt on + * the next frame. + */ +export type PairScanState = + | { status: "scanning" } + | { status: "pairing" } + | { status: "passwordRequired"; passwordRequired: PasswordRequiredPairing } + | { status: "stopped"; error: string | null } + | { status: "connected" }; + +export interface PairScanModel { + getState: () => PairScanState; + subscribe: (listener: () => void) => () => void; + /** Data from a QR code the camera saw. Only a scanning session starts an attempt. */ + scan: (data: string) => void; + /** The user asked to scan again after an attempt ended. */ + scanAgain: () => void; + /** The user closed the password prompt, which ends the attempt. */ + closePassword: () => void; + close: () => void; +} + +export interface PairScanDependencies { + importConnectionLink: (link: string) => Promise; + onConnected: (serverId: string) => void; +} + +export function openPairScan(deps: PairScanDependencies): PairScanModel { + let state: PairScanState = { status: "scanning" }; + let closed = false; + const listeners = new Set<() => void>(); + + function publish(next: PairScanState): void { + if (closed) return; + state = next; + for (const listener of listeners) { + listener(); + } + } + + async function pair(link: string): Promise { + publish({ status: "pairing" }); + try { + const outcome = await deps.importConnectionLink(link); + if (closed) return; + if (outcome.status === "connected") { + publish({ status: "connected" }); + deps.onConnected(outcome.serverId); + return; + } + if (outcome.status === "password_required") { + const { link: passwordLink, pairing } = outcome; + publish({ status: "passwordRequired", passwordRequired: { link: passwordLink, pairing } }); + return; + } + publish({ status: "stopped", error: null }); + } catch (error) { + publish({ status: "stopped", error: error instanceof Error ? error.message : String(error) }); + } + } + + return { + getState: () => state, + subscribe(listener) { + listeners.add(listener); + return () => { + listeners.delete(listener); + }; + }, + scan(data) { + if (state.status !== "scanning") return; + const link = pairingLinkFromScan(data); + if (!link) return; + void pair(link); + }, + scanAgain() { + if (state.status !== "stopped") return; + publish({ status: "scanning" }); + }, + closePassword() { + if (state.status !== "passwordRequired") return; + publish({ status: "stopped", error: null }); + }, + close() { + closed = true; + listeners.clear(); + }, + }; +} + +function pairingLinkFromScan(data: string): string | null { + const raw = data.trim(); + if (raw.includes("#offer=") || raw.includes("#connect=") || raw.startsWith("relay://")) { + return raw; + } + return null; +} diff --git a/packages/app/src/i18n/resources/ar.ts b/packages/app/src/i18n/resources/ar.ts index 658c1981df..07f3a6d00d 100644 --- a/packages/app/src/i18n/resources/ar.ts +++ b/packages/app/src/i18n/resources/ar.ts @@ -1624,6 +1624,17 @@ export const ar: TranslationResources = { title: "كلمة المرور لـ {{host}}", label: "كلمة مرور المضيف", }, + hostConfirmation: { + title: "الاتصال بهذا المضيف؟", + description: + "سيتمكن هذا المضيف من تشغيل تعليمات برمجية في هذا التطبيق والوصول إلى مضيفيك الآخرين المتصلين. اتصل فقط إذا كنت تعرفه.", + descriptionChanged: + "يغيّر هذا الرابط طريقة اتصالك بهذا المضيف. سيتمكن المضيف من تشغيل تعليمات برمجية في هذا التطبيق والوصول إلى مضيفيك الآخرين المتصلين. اتصل فقط إذا كنت تعرفه.", + hostLabel: "المضيف", + fingerprintLabel: "بصمة المفتاح", + relayLabel: "المُرحّل", + connect: "اتصال", + }, connectionMethods: { title: "إضافة اتصال", direct: { diff --git a/packages/app/src/i18n/resources/en.ts b/packages/app/src/i18n/resources/en.ts index 80a137ccf3..9230ef3155 100644 --- a/packages/app/src/i18n/resources/en.ts +++ b/packages/app/src/i18n/resources/en.ts @@ -1649,6 +1649,17 @@ export const en = { title: "Password for {{host}}", label: "Host password", }, + hostConfirmation: { + title: "Connect to this host?", + description: + "This host will be able to run code in this app and reach your other connected hosts. Only connect if you recognize it.", + descriptionChanged: + "This link changes how you connect to this host. The host will be able to run code in this app and reach your other connected hosts. Only connect if you recognize it.", + hostLabel: "Host", + fingerprintLabel: "Key fingerprint", + relayLabel: "Relay", + connect: "Connect", + }, connectionMethods: { title: "Add connection", direct: { diff --git a/packages/app/src/i18n/resources/es.ts b/packages/app/src/i18n/resources/es.ts index 74ac40906b..c61778633d 100644 --- a/packages/app/src/i18n/resources/es.ts +++ b/packages/app/src/i18n/resources/es.ts @@ -1670,6 +1670,17 @@ export const es: TranslationResources = { title: "Contraseña de {{host}}", label: "Contraseña del host", }, + hostConfirmation: { + title: "¿Conectar con este host?", + description: + "Este host podrá ejecutar código en esta app y acceder a tus otros hosts conectados. Conéctate solo si lo reconoces.", + descriptionChanged: + "Este enlace cambia cómo te conectas a este host. El host podrá ejecutar código en esta app y acceder a tus otros hosts conectados. Conéctate solo si lo reconoces.", + hostLabel: "Host", + fingerprintLabel: "Huella de la clave", + relayLabel: "Relay", + connect: "Conectar", + }, connectionMethods: { title: "Agregar conexión", direct: { diff --git a/packages/app/src/i18n/resources/fr.ts b/packages/app/src/i18n/resources/fr.ts index d5b811f49c..6e6999e5f8 100644 --- a/packages/app/src/i18n/resources/fr.ts +++ b/packages/app/src/i18n/resources/fr.ts @@ -1674,6 +1674,17 @@ export const fr: TranslationResources = { title: "Mot de passe pour {{host}}", label: "Mot de passe de l’hôte", }, + hostConfirmation: { + title: "Se connecter à cet hôte ?", + description: + "Cet hôte pourra exécuter du code dans cette application et accéder à vos autres hôtes connectés. Connectez-vous uniquement si vous le reconnaissez.", + descriptionChanged: + "Ce lien modifie la façon dont vous vous connectez à cet hôte. L'hôte pourra exécuter du code dans cette application et accéder à vos autres hôtes connectés. Connectez-vous uniquement si vous le reconnaissez.", + hostLabel: "Hôte", + fingerprintLabel: "Empreinte de la clé", + relayLabel: "Relais", + connect: "Se connecter", + }, connectionMethods: { title: "Ajouter une connexion", direct: { diff --git a/packages/app/src/i18n/resources/ja.ts b/packages/app/src/i18n/resources/ja.ts index b674fec325..8e59c63234 100644 --- a/packages/app/src/i18n/resources/ja.ts +++ b/packages/app/src/i18n/resources/ja.ts @@ -1641,6 +1641,17 @@ export const ja: TranslationResources = { title: "{{host}} のパスワード", label: "ホストのパスワード", }, + hostConfirmation: { + title: "このホストに接続しますか?", + description: + "このホストはこのアプリ内でコードを実行し、接続中の他のホストにアクセスできるようになります。心当たりがある場合のみ接続してください。", + descriptionChanged: + "このリンクはこのホストへの接続方法を変更します。ホストはこのアプリ内でコードを実行し、接続中の他のホストにアクセスできるようになります。心当たりがある場合のみ接続してください。", + hostLabel: "ホスト", + fingerprintLabel: "鍵のフィンガープリント", + relayLabel: "リレー", + connect: "接続", + }, connectionMethods: { title: "接続を追加", direct: { diff --git a/packages/app/src/i18n/resources/ko.ts b/packages/app/src/i18n/resources/ko.ts index 5f2aa560fd..0f9437720d 100644 --- a/packages/app/src/i18n/resources/ko.ts +++ b/packages/app/src/i18n/resources/ko.ts @@ -1634,6 +1634,17 @@ export const ko: TranslationResources = { title: "{{host}}의 비밀번호", label: "호스트 비밀번호", }, + hostConfirmation: { + title: "이 호스트에 연결할까요?", + description: + "이 호스트는 이 앱에서 코드를 실행하고 연결된 다른 호스트에 접근할 수 있게 됩니다. 알고 있는 호스트인 경우에만 연결하세요.", + descriptionChanged: + "이 링크는 이 호스트에 연결하는 방식을 변경합니다. 호스트는 이 앱에서 코드를 실행하고 연결된 다른 호스트에 접근할 수 있게 됩니다. 알고 있는 호스트인 경우에만 연결하세요.", + hostLabel: "호스트", + fingerprintLabel: "키 지문", + relayLabel: "릴레이", + connect: "연결", + }, connectionMethods: { title: "연결 추가", direct: { diff --git a/packages/app/src/i18n/resources/pt-BR.ts b/packages/app/src/i18n/resources/pt-BR.ts index bf3704bf63..75249797a2 100644 --- a/packages/app/src/i18n/resources/pt-BR.ts +++ b/packages/app/src/i18n/resources/pt-BR.ts @@ -1655,6 +1655,17 @@ export const ptBR: TranslationResources = { title: "Senha de {{host}}", label: "Senha do host", }, + hostConfirmation: { + title: "Conectar a este host?", + description: + "Este host poderá executar código neste app e acessar seus outros hosts conectados. Conecte apenas se você o reconhecer.", + descriptionChanged: + "Este link muda como você se conecta a este host. O host poderá executar código neste app e acessar seus outros hosts conectados. Conecte apenas se você o reconhecer.", + hostLabel: "Host", + fingerprintLabel: "Impressão da chave", + relayLabel: "Relay", + connect: "Conectar", + }, connectionMethods: { title: "Adicionar conexão", direct: { diff --git a/packages/app/src/i18n/resources/ru.ts b/packages/app/src/i18n/resources/ru.ts index ed8521e540..af11db8bf1 100644 --- a/packages/app/src/i18n/resources/ru.ts +++ b/packages/app/src/i18n/resources/ru.ts @@ -1653,6 +1653,17 @@ export const ru: TranslationResources = { title: "Пароль для {{host}}", label: "Пароль хоста", }, + hostConfirmation: { + title: "Подключиться к этому хосту?", + description: + "Этот хост сможет выполнять код в приложении и обращаться к другим вашим подключённым хостам. Подключайтесь, только если узнаёте его.", + descriptionChanged: + "Эта ссылка меняет способ подключения к этому хосту. Хост сможет выполнять код в приложении и обращаться к другим вашим подключённым хостам. Подключайтесь, только если узнаёте его.", + hostLabel: "Хост", + fingerprintLabel: "Отпечаток ключа", + relayLabel: "Реле", + connect: "Подключить", + }, connectionMethods: { title: "Добавить подключение", direct: { diff --git a/packages/app/src/i18n/resources/zh-CN.ts b/packages/app/src/i18n/resources/zh-CN.ts index d01d7f5130..3bc8727cb4 100644 --- a/packages/app/src/i18n/resources/zh-CN.ts +++ b/packages/app/src/i18n/resources/zh-CN.ts @@ -1607,6 +1607,16 @@ export const zhCN: TranslationResources = { title: "{{host}} 的密码", label: "主机密码", }, + hostConfirmation: { + title: "连接到此主机?", + description: "此主机将能在此应用中运行代码,并访问你其他已连接的主机。仅在你认识它时才连接。", + descriptionChanged: + "此链接会改变你连接此主机的方式。该主机将能在此应用中运行代码,并访问你其他已连接的主机。仅在你认识它时才连接。", + hostLabel: "主机", + fingerprintLabel: "密钥指纹", + relayLabel: "中继", + connect: "连接", + }, connectionMethods: { title: "添加连接", direct: { diff --git a/packages/app/src/runtime/host-confirmation.ts b/packages/app/src/runtime/host-confirmation.ts new file mode 100644 index 0000000000..8fac1031ff --- /dev/null +++ b/packages/app/src/runtime/host-confirmation.ts @@ -0,0 +1,97 @@ +import type { ConnectionOffer } from "@getpaseo/protocol/connection-offer"; +import { + hostHasConnection, + relayConnectionFromOffer, + type HostProfile, +} from "@/types/host-connection"; + +/** + * The question shown before a pairing link adds a host that is not saved, or + * changes the key, relay, or TLS setting of a saved one. + */ +export interface HostConfirmationRequest { + /** Identifies this question, so an answer shown for it never answers a newer one. */ + id: number; + serverId: string; + /** Grouped daemon public key the user can compare with the one their daemon shows. */ + keyFingerprint: string; + relayEndpoint: string; + kind: "newHost" | "changedConnection"; +} + +interface PendingConfirmation { + request: HostConfirmationRequest; + resolve: (approved: boolean) => void; +} + +/** + * Owns the one host confirmation the app is waiting on. The host runtime asks + * through `confirmLink`; the view reads `getPending` and replies with `answer`. + * A question waits until it is answered, whether or not a view is showing it. + */ +export class HostConfirmations { + private pending: PendingConfirmation | null = null; + private nextRequestId = 1; + private listeners = new Set<() => void>(); + + getPending(): HostConfirmationRequest | null { + return this.pending?.request ?? null; + } + + subscribe(listener: () => void): () => void { + this.listeners.add(listener); + return () => { + this.listeners.delete(listener); + }; + } + + /** Answers the request with `requestId`; an answer for any other request is ignored. */ + answer(requestId: number, approved: boolean): void { + const current = this.pending; + if (!current || current.request.id !== requestId) return; + this.pending = null; + this.emit(); + current.resolve(approved); + } + + /** Resolves true without asking when the link matches a saved host exactly. */ + confirmLink(offer: ConnectionOffer, savedHosts: HostProfile[]): Promise { + const connection = relayConnectionFromOffer(offer); + const savedHost = savedHosts.find((host) => host.serverId === offer.serverId); + if (savedHost && hostHasConnection(savedHost, connection)) return Promise.resolve(true); + return this.ask({ + id: this.nextRequestId++, + serverId: offer.serverId, + keyFingerprint: formatDaemonKeyFingerprint(connection.daemonPublicKeyB64), + relayEndpoint: connection.relayEndpoint, + kind: savedHost ? "changedConnection" : "newHost", + }); + } + + private ask(request: HostConfirmationRequest): Promise { + // One question at a time: a newer one cancels the unanswered one. + this.pending?.resolve(false); + return new Promise((resolve) => { + this.pending = { request, resolve }; + this.emit(); + }); + } + + private emit(): void { + for (const listener of this.listeners) { + listener(); + } + } +} + +/** + * Shows the key material itself (not a re-hash) in groups of four. Keys longer + * than 16 characters show only their first and last 8, so two keys that share + * both ends show the same value. + */ +function formatDaemonKeyFingerprint(daemonPublicKeyB64: string): string { + const normalized = daemonPublicKeyB64.replace(/[^A-Za-z0-9]/g, ""); + const group = (value: string): string => value.replace(/(.{4})/g, "$1 ").trim(); + if (normalized.length <= 16) return group(normalized); + return `${group(normalized.slice(0, 8))} … ${group(normalized.slice(-8))}`; +} diff --git a/packages/app/src/runtime/host-runtime.test.ts b/packages/app/src/runtime/host-runtime.test.ts index fbf7764f31..a3ea10f48b 100644 --- a/packages/app/src/runtime/host-runtime.test.ts +++ b/packages/app/src/runtime/host-runtime.test.ts @@ -31,6 +31,7 @@ import type { ReplicaRow, ReplicaRowStore } from "./replica-cache/row-store"; import { subscriptionFixture } from "./subscription-fixture"; import { readDesktopManagedLocalCredential } from "@/desktop/daemon/local-credential"; +import type { HostConfirmationRequest } from "./host-confirmation"; it("requests the managed connection credential through desktop main without a web hint", async () => { const requests: string[] = []; @@ -507,6 +508,63 @@ function makeConnectedProbeClient(latencyMs: number): FakeDaemonClient { return client; } +/** + * Answers every host confirmation the store asks, the way the sheet does, and + * records each request. + */ +function answerHostConfirmations( + store: HostRuntimeStore, + answer: boolean | ((request: HostConfirmationRequest, index: number) => boolean), +): HostConfirmationRequest[] { + const requests: HostConfirmationRequest[] = []; + store.subscribeHostConfirmation(() => { + const pending = store.getPendingHostConfirmation(); + if (!pending) return; + requests.push(pending); + store.answerHostConfirmation( + pending.id, + typeof answer === "function" ? answer(pending, requests.length - 1) : answer, + ); + }); + return requests; +} + +function requirePendingConfirmation(store: HostRuntimeStore): HostConfirmationRequest { + const pending = store.getPendingHostConfirmation(); + if (!pending) throw new Error("Expected a pending host confirmation"); + return pending; +} + +function createPairingStore( + input: { + hostname?: string; + password?: string; + connectAttempts?: string[]; + serverIdForHost?: (host: HostProfile) => string; + storage?: HostRuntimeStorage; + } = {}, +): HostRuntimeStore { + return new HostRuntimeStore({ + storage: input.storage ?? createMemoryHostRuntimeStorage(), + deps: { + createClient: () => new FakeDaemonClient() as unknown as DaemonClient, + connectToDaemon: async ({ host, connection }) => { + input.connectAttempts?.push(connection.id); + if (input.password && host.password !== input.password) { + throw new DaemonAuthenticationError("password_required"); + } + return { + client: makeConnectedProbeClient(5) as unknown as DaemonClient, + serverId: input.serverIdForHost?.(host) ?? host.serverId, + hostname: input.hostname ?? "paired", + }; + }, + getClientId: async () => "cid_pairing", + readInitialConnectionHint: () => null, + }, + }); +} + function createMemoryHostRuntimeStorage(entries: Record = {}): HostRuntimeStorage { const values = new Map(Object.entries(entries)); return { @@ -3547,19 +3605,10 @@ describe("HostRuntimeStore", () => { }); it("uses the advertised hostname when adding a relay host from a pairing offer", async () => { - const store = new HostRuntimeStore({ - deps: { - createClient: () => new FakeDaemonClient() as unknown as DaemonClient, - connectToDaemon: async ({ host }) => ({ - client: makeConnectedProbeClient(5) as unknown as DaemonClient, - serverId: host.serverId, - hostname: host.label ?? null, - }), - getClientId: async () => "cid_test_runtime", - }, - }); + const store = createPairingStore({ hostname: "mbp" }); + answerHostConfirmations(store, true); - await store.upsertConnectionFromOffer(makeOffer(), "mbp"); + await store.beginLinkPairing().submit(encodeOfferUrl(makeOffer())); const pairedHost = store.getHosts().find((host) => host.serverId === "srv_offer"); expect(pairedHost?.label).toBe("mbp"); @@ -3568,27 +3617,14 @@ describe("HostRuntimeStore", () => { }); it("stores relay TLS from a pairing offer", async () => { - const store = new HostRuntimeStore({ - deps: { - createClient: () => new FakeDaemonClient() as unknown as DaemonClient, - connectToDaemon: async ({ host }) => ({ - client: makeConnectedProbeClient(5) as unknown as DaemonClient, - serverId: host.serverId, - hostname: host.label ?? null, - }), - getClientId: async () => "cid_test_runtime", - }, - }); + const store = createPairingStore(); + answerHostConfirmations(store, true); - await store.upsertConnectionFromOffer( - makeOffer({ - relay: { - endpoint: "relay.example.com:443", - useTls: true, - }, - }), - "tls relay", - ); + await store + .beginLinkPairing() + .submit( + encodeOfferUrl(makeOffer({ relay: { endpoint: "relay.example.com:443", useTls: true } })), + ); const pairedHost = store.getHosts().find((host) => host.serverId === "srv_offer"); expect(pairedHost?.connections).toEqual([ @@ -3605,17 +3641,8 @@ describe("HostRuntimeStore", () => { }); it("uses TLS for old pairing URLs that omit relay TLS on port 443", async () => { - const store = new HostRuntimeStore({ - deps: { - createClient: () => new FakeDaemonClient() as unknown as DaemonClient, - connectToDaemon: async ({ host }) => ({ - client: makeConnectedProbeClient(5) as unknown as DaemonClient, - serverId: host.serverId, - hostname: host.label ?? null, - }), - getClientId: async () => "cid_test_runtime", - }, - }); + const store = createPairingStore(); + answerHostConfirmations(store, true); const oldPairingUrl = encodeOfferUrl({ v: 2, serverId: "srv_offer", @@ -3623,7 +3650,7 @@ describe("HostRuntimeStore", () => { relay: { endpoint: "relay.paseo.sh:443" }, }); - await store.upsertConnectionFromOfferUrl(oldPairingUrl, "old relay"); + await store.importConnectionLink(oldPairingUrl, "openProject"); const pairedHost = store.getHosts().find((host) => host.serverId === "srv_offer"); expect(pairedHost?.connections).toEqual([ @@ -3640,54 +3667,266 @@ describe("HostRuntimeStore", () => { }); it("probes a pairing link immediately and saves only after admission", async () => { - const store = new HostRuntimeStore({ - deps: { - createClient: () => new FakeDaemonClient() as unknown as DaemonClient, - connectToDaemon: async ({ host }) => { - if (host.password !== "correct-password") - throw new DaemonAuthenticationError("password_required"); - return { - client: makeConnectedProbeClient(5) as unknown as DaemonClient, - serverId: host.serverId, - hostname: "paired host", - }; - }, - getClientId: async () => "cid_pairing", - }, - }); + const store = createPairingStore({ password: "correct-password" }); + answerHostConfirmations(store, true); const offerUrl = encodeOfferUrl(makeOffer()); - await expect(store.probeAndUpsertConnectionFromOfferUrl(offerUrl)).rejects.toThrow( - "Password required", - ); + const pairing = store.beginLinkPairing(); + + await expect(pairing.submit(offerUrl)).rejects.toThrow("Password required"); expect(store.getHosts()).toHaveLength(0); - const result = await store.probeAndUpsertConnectionFromOfferUrl(offerUrl, "correct-password"); - expect(result.serverId).toBe("srv_offer"); + const result = await pairing.submit(offerUrl, "correct-password"); + expect(result).toMatchObject({ status: "connected", serverId: "srv_offer" }); expect(store.getHosts()[0]?.password).toBe("correct-password"); store.syncHosts([]); }); - it("preserves the existing host label when re-pairing an existing relay host", async () => { - const store = new HostRuntimeStore({ - deps: { - createClient: () => new FakeDaemonClient() as unknown as DaemonClient, - connectToDaemon: async ({ host }) => ({ - client: makeConnectedProbeClient(5) as unknown as DaemonClient, - serverId: host.serverId, - hostname: host.label ?? null, - }), - getClientId: async () => "cid_test_runtime", + it("asks before a pairing link adds a new host, and Cancel saves and connects nothing", async () => { + const connectAttempts: string[] = []; + const store = createPairingStore({ connectAttempts }); + const requests = answerHostConfirmations(store, false); + const offerUrl = encodeOfferUrl(makeOffer()); + const relayLink = "relay://relay.example:443/srv_relay?key=AAAA&ssl=true"; + + // `#offer=` in the app URL, then a QR scan, then the pair-link and add-host modals. + await expect(store.importConnectionLink(offerUrl, "openProject")).resolves.toEqual({ + status: "cancelled", + }); + await expect(store.importConnectionLink(offerUrl, "hostRoot")).resolves.toEqual({ + status: "cancelled", + }); + await expect(store.beginLinkPairing().submit(offerUrl)).resolves.toEqual({ + status: "cancelled", + }); + await expect(store.beginLinkPairing().submit(relayLink)).resolves.toEqual({ + status: "cancelled", + }); + + expect(requests).toEqual([ + ...Array.from({ length: 3 }, () => ({ + id: expect.any(Number), + serverId: "srv_offer", + keyFingerprint: "pkte stof fer", + relayEndpoint: "relay.paseo.sh:443", + kind: "newHost", + })), + { + id: expect.any(Number), + serverId: "srv_relay", + keyFingerprint: "AAAA", + relayEndpoint: "relay.example:443", + kind: "newHost", }, - storage: createMemoryHostRuntimeStorage(), - }); + ]); + expect(store.getHosts()).toEqual([]); + expect(connectAttempts).toEqual([]); + store.syncHosts([]); + }); - await store.upsertRelayConnection({ + it("connects a saved host from a matching pairing link without asking", async () => { + const store = createPairingStore(); + const requests = answerHostConfirmations(store, true); + const offerUrl = encodeOfferUrl(makeOffer()); + await store.beginLinkPairing().submit(offerUrl); + expect(requests).toHaveLength(1); + + await expect(store.importConnectionLink(offerUrl, "openProject")).resolves.toEqual({ + status: "connected", serverId: "srv_offer", - relayEndpoint: "relay.paseo.sh:443", - daemonPublicKeyB64: "pk_test_offer", - label: "Custom name", + }); + await expect(store.importConnectionLink(offerUrl, "hostRoot")).resolves.toEqual({ + status: "connected", + serverId: "srv_offer", + }); + await expect(store.beginLinkPairing().submit(offerUrl)).resolves.toMatchObject({ + status: "connected", }); - await store.upsertConnectionFromOffer(makeOffer(), "mbp"); + expect(requests).toHaveLength(1); + expect(store.getHosts()).toHaveLength(1); + store.syncHosts([]); + }); + + it("asks when a pairing link changes a saved host's key, relay, or TLS setting", async () => { + const store = createPairingStore(); + const requests = answerHostConfirmations(store, (_request, index) => index === 0); + await store.beginLinkPairing().submit(encodeOfferUrl(makeOffer())); + const savedHosts = store.getHosts(); + + const changedLinks = [ + makeOffer({ daemonPublicKeyB64: "pk_other_key" }), + makeOffer({ relay: { endpoint: "relay.example:443", useTls: false } }), + makeOffer({ relay: { endpoint: "relay.paseo.sh:443", useTls: true } }), + ].map(encodeOfferUrl); + for (const link of changedLinks) { + await expect(store.importConnectionLink(link, "openProject")).resolves.toEqual({ + status: "cancelled", + }); + await expect(store.beginLinkPairing().submit(link)).resolves.toEqual({ + status: "cancelled", + }); + } + + expect(requests.map((request) => request.kind)).toEqual([ + "newHost", + ...Array.from({ length: 6 }, () => "changedConnection"), + ]); + expect(store.getHosts()).toEqual(savedHosts); + store.syncHosts([]); + }); + + it("asks once per pairing flow and link when the host asks for a password", async () => { + const store = createPairingStore({ password: "correct-password" }); + const requests = answerHostConfirmations(store, (_request, index) => index < 2); + const offerUrl = encodeOfferUrl(makeOffer()); + const pairing = store.beginLinkPairing(); + + await expect(pairing.submit(offerUrl)).rejects.toThrow("Password required"); + await expect(pairing.submit(offerUrl, "wrong-password")).rejects.toThrow(); + expect(requests).toHaveLength(1); + + // A new pairing flow for the same link asks again. + await expect(store.beginLinkPairing().submit(offerUrl, "wrong-password")).rejects.toThrow(); + expect(requests).toHaveLength(2); + + await expect(pairing.submit(offerUrl, "correct-password")).resolves.toMatchObject({ + status: "connected", + }); + expect(requests).toHaveLength(2); + expect(store.getHosts()[0]).toMatchObject({ + serverId: "srv_offer", + password: "correct-password", + }); + + // A different link in the same flow asks again. + const otherKeyUrl = encodeOfferUrl(makeOffer({ daemonPublicKeyB64: "pk_other_key" })); + await expect(pairing.submit(otherKeyUrl, "correct-password")).resolves.toEqual({ + status: "cancelled", + }); + expect(requests).toHaveLength(3); + store.syncHosts([]); + }); + + it("continues a scanned link's password retry without asking again", async () => { + const store = createPairingStore({ password: "correct-password" }); + const requests = answerHostConfirmations(store, true); + const link = "relay://relay.example:443/srv_pair?key=AAAA&ssl=true"; + + const outcome = await store.importConnectionLink(link, "hostRoot"); + expect(outcome).toMatchObject({ status: "password_required", link }); + expect(store.getHosts()).toHaveLength(0); + if (outcome.status !== "password_required") throw new Error("expected password_required"); + + await outcome.pairing.submit(outcome.link, "correct-password"); + expect(requests).toHaveLength(1); + expect(store.getHosts()[0]).toMatchObject({ + serverId: "srv_pair", + password: "correct-password", + }); + store.syncHosts([]); + }); + + it("keeps a host confirmation pending until it is answered, and a newer one cancels it", async () => { + const store = createPairingStore(); + const first = store.importConnectionLink(encodeOfferUrl(makeOffer()), "openProject"); + await vi.waitFor(() => + expect(store.getPendingHostConfirmation()).toMatchObject({ serverId: "srv_offer" }), + ); + + const second = store.importConnectionLink( + encodeOfferUrl(makeOffer({ serverId: "srv_second" })), + "openProject", + ); + await expect(first).resolves.toEqual({ status: "cancelled" }); + await vi.waitFor(() => + expect(store.getPendingHostConfirmation()).toMatchObject({ serverId: "srv_second" }), + ); + + store.answerHostConfirmation(requirePendingConfirmation(store).id, true); + await expect(second).resolves.toEqual({ status: "connected", serverId: "srv_second" }); + expect(store.getPendingHostConfirmation()).toBeNull(); + expect(store.getHosts().map((host) => host.serverId)).toEqual(["srv_second"]); + store.syncHosts([]); + }); + + it("ignores an answer for a host confirmation that a newer one replaced", async () => { + const store = createPairingStore(); + const first = store.importConnectionLink(encodeOfferUrl(makeOffer()), "openProject"); + await vi.waitFor(() => + expect(store.getPendingHostConfirmation()).toMatchObject({ serverId: "srv_offer" }), + ); + const firstRequest = requirePendingConfirmation(store); + const second = store.importConnectionLink( + encodeOfferUrl(makeOffer({ serverId: "srv_second" })), + "openProject", + ); + await expect(first).resolves.toEqual({ status: "cancelled" }); + + // Connect tapped on the first sheet as the second link arrives. + store.answerHostConfirmation(firstRequest.id, true); + + const secondRequest = requirePendingConfirmation(store); + expect(secondRequest.serverId).toBe("srv_second"); + store.answerHostConfirmation(secondRequest.id, false); + await expect(second).resolves.toEqual({ status: "cancelled" }); + expect(store.getHosts()).toEqual([]); + }); + + it("waits for saved hosts to load before deciding whether a pairing link asks", async () => { + const savedHost = makeHost({ + serverId: "srv_offer", + connections: [ + { + id: "relay:relay.paseo.sh:443", + type: "relay", + relayEndpoint: "relay.paseo.sh:443", + useTls: false, + daemonPublicKeyB64: "pk_test_offer", + }, + ], + }); + const store = createPairingStore({ + storage: createMemoryHostRuntimeStorage({ + "@paseo:daemon-registry": JSON.stringify([savedHost]), + "@paseo:e2e": "1", + }), + }); + const requests = answerHostConfirmations(store, true); + + await expect( + store.importConnectionLink(encodeOfferUrl(makeOffer()), "openProject"), + ).resolves.toEqual({ status: "connected", serverId: "srv_offer" }); + + expect(requests).toEqual([]); + expect(store.getHosts().map((host) => host.serverId)).toEqual(["srv_offer"]); + store.syncHosts([]); + }); + + it("adds hosts by address and the default localhost without asking", async () => { + const store = createPairingStore({ + serverIdForHost: (host) => + host.connections[0]?.type === "directTcp" ? `srv_${host.connections[0].endpoint}` : "", + }); + const requests = answerHostConfirmations(store, false); + + await store.boot(); + await store.probeAndUpsertDirectConnection({ endpoint: "10.0.0.5:6767" }); + + expect(requests).toEqual([]); + expect(store.getHosts().map((host) => host.serverId)).toEqual([ + "srv_localhost:6767", + "srv_10.0.0.5:6767", + ]); + store.syncHosts([]); + }); + + it("preserves the existing host label when re-pairing an existing relay host", async () => { + const store = createPairingStore({ hostname: "mbp" }); + answerHostConfirmations(store, true); + + await store.beginLinkPairing().submit(encodeOfferUrl(makeOffer())); + await store.renameHost("srv_offer", "Custom name"); + + await store.beginLinkPairing().submit(encodeOfferUrl(makeOffer())); const pairedHost = store.getHosts().find((host) => host.serverId === "srv_offer"); expect(pairedHost?.label).toBe("Custom name"); @@ -3963,35 +4202,6 @@ describe("HostRuntimeStore initial connection hint bootstrap", () => { expect(probeAttempts).toBe(attemptsBeforeRetry); store.syncHosts([]); }); - it("imports a pairing link after a password retry in the add flow", async () => { - const store = new HostRuntimeStore({ - storage: createMemoryHostRuntimeStorage(), - deps: { - createClient: () => new FakeDaemonClient() as unknown as DaemonClient, - connectToDaemon: async ({ host }) => { - if (host.password !== "correct-password") - throw new DaemonAuthenticationError("password_required"); - return { - client: makeConnectedProbeClient(5) as unknown as DaemonClient, - serverId: host.serverId, - hostname: "paired", - }; - }, - getClientId: async () => "cid_link_import", - }, - }); - const link = "relay://relay.example:443/srv_pair?key=AAAA&ssl=true"; - await expect(store.importConnectionLink(link, "hostRoot")).resolves.toEqual({ - status: "password_required", - }); - expect(store.getHosts()).toHaveLength(0); - await store.probeAndUpsertConnectionFromOfferUrl(link, "correct-password"); - expect(store.getHosts()[0]).toMatchObject({ - serverId: "srv_pair", - password: "correct-password", - }); - store.syncHosts([]); - }); it("imports a deep link without waiting for admission, then leaves a rejected saved host red", async () => { const store = new HostRuntimeStore({ storage: createMemoryHostRuntimeStorage(), @@ -4003,6 +4213,7 @@ describe("HostRuntimeStore initial connection hint bootstrap", () => { getClientId: async () => "cid_deep_link", }, }); + answerHostConfirmations(store, true); const link = "relay://relay.example:443/srv_deep?key=AAAA&ssl=true"; await expect(store.importConnectionLink(link, "openProject")).resolves.toEqual({ status: "connected", diff --git a/packages/app/src/runtime/host-runtime.ts b/packages/app/src/runtime/host-runtime.ts index 14668aa0b5..1e0fdb7cad 100644 --- a/packages/app/src/runtime/host-runtime.ts +++ b/packages/app/src/runtime/host-runtime.ts @@ -14,6 +14,7 @@ import { normalizeStoredHostProfile, upsertHostConnectionInProfiles, registryHasConnection, + relayConnectionFromOffer, StoredHostRegistrySchema, type HostConnection, type HostProfile, @@ -29,6 +30,7 @@ import { } from "@/utils/daemon-endpoints"; import { resolveAppVersion } from "@/utils/app-version"; import { ConnectionOfferSchema, type ConnectionOffer } from "@getpaseo/protocol/connection-offer"; +import { HostConfirmations, type HostConfirmationRequest } from "./host-confirmation"; import { shouldUseDesktopDaemon } from "@/desktop/daemon/desktop-daemon"; import { isWeb } from "@/constants/platform"; import { connectToDaemon, getConnectionAuthFailureReason } from "@/utils/test-daemon-connection"; @@ -89,6 +91,27 @@ export type HostRuntimeConnectionStatus = "idle" | "connecting" | "online" | "of export type PairingNavigationTarget = "openProject" | "hostRoot" | "hostSettings"; export type HostRegistryStatus = "loading" | "ready"; +/** One pairing flow started with `HostRuntimeStore.beginLinkPairing`. */ +export interface LinkPairing { + /** Probes the linked host and saves it. Connection and password errors throw. */ + submit(link: string, password?: string): Promise; +} + +export type LinkPairingResult = + | { status: "connected"; profile: HostProfile; serverId: string; hostname: string | null } + | { status: "cancelled" }; + +/** A pairing link the user confirmed whose host then asked for a password. */ +export interface PasswordRequiredPairing { + link: string; + pairing: LinkPairing; +} + +export type ConnectionLinkImport = + | { status: "connected"; serverId: string } + | ({ status: "password_required" } & PasswordRequiredPairing) + | { status: "cancelled" }; + export type ActiveConnection = | { type: "directTcp"; endpoint: string; display: string } | { type: "directSocket"; endpoint: string; display: "socket" } @@ -1415,6 +1438,13 @@ function readConfiguredLocalDaemonOverride(): string | null { return value && value.length > 0 ? value : null; } +function isSameLinkedConnection(left: ConnectionOffer, right: ConnectionOffer): boolean { + return ( + left.serverId === right.serverId && + equal(relayConnectionFromOffer(left), relayConnectionFromOffer(right)) + ); +} + function parseOfferConnectionUrl(input: string): { offer: ConnectionOffer; password?: string } { if (input.trim().startsWith("relay://") || input.includes("#connect=")) { return parseRelayConnectionUri(input); @@ -1476,6 +1506,8 @@ export class HostRuntimeStore { private timelineReplicaByServer = new Map(); private configuredOverrideBootstrapInFlight: Promise | null = null; private bootPromise: Promise | null = null; + private registryLoad: Promise | null = null; + private readonly hostConfirmations = new HostConfirmations(); private storage: HostRuntimeStorage; private replicaCache: ReplicaCache; private readonly revokePushNotifications: typeof revokePushNotifications; @@ -1526,8 +1558,7 @@ export class HostRuntimeStore { private async runBoot(): Promise { const override = readConfiguredLocalDaemonOverride(); - await this.loadFromStorage(); - this.markHostRegistryLoaded(); + await this.loadRegistry(); let isE2E: string | null = null; try { @@ -1560,6 +1591,13 @@ export class HostRuntimeStore { } } + private loadRegistry(): Promise { + if (!this.registryLoad) { + this.registryLoad = this.loadFromStorage().then(() => this.markHostRegistryLoaded()); + } + return this.registryLoad; + } + private async loadFromStorage(): Promise { let shouldPersistHosts = false; let profiles: HostProfile[] = []; @@ -1886,7 +1924,7 @@ export class HostRuntimeStore { }); } - async upsertRelayConnection(input: { + private async upsertRelayConnection(input: { serverId: string; relayEndpoint: string; useTls?: boolean; @@ -1919,51 +1957,91 @@ export class HostRuntimeStore { return profile; } - async upsertConnectionFromOffer( + // A pairing link (a URL, a QR code, or a pasted link) asks the user to + // confirm when it adds a new host or changes a saved host's key, relay, or + // TLS setting. The answer needs the saved hosts, so it waits for them to load. + private async confirmLink(offer: ConnectionOffer): Promise { + await this.loadRegistry(); + return this.hostConfirmations.confirmLink(offer, this.hosts); + } + + getPendingHostConfirmation(): HostConfirmationRequest | null { + return this.hostConfirmations.getPending(); + } + + subscribeHostConfirmation(listener: () => void): () => void { + return this.hostConfirmations.subscribe(listener); + } + + answerHostConfirmation(requestId: number, approved: boolean): void { + this.hostConfirmations.answer(requestId, approved); + } + + /** + * Starts one pairing flow, such as one open pairing modal. Once the user + * confirms a link, submitting the same link again in this flow (the + * password retry) does not ask again. A new flow asks again. + */ + beginLinkPairing(): LinkPairing { + let approvedOffer: ConnectionOffer | null = null; + return { + submit: async (link, password) => { + const { offer, password: linkPassword } = parseOfferConnectionUrl(link); + if (!approvedOffer || !isSameLinkedConnection(approvedOffer, offer)) { + if (!(await this.confirmLink(offer))) return { status: "cancelled" }; + approvedOffer = offer; + } + const result = await this.probeAndUpsertOffer(offer, password ?? linkPassword); + return { status: "connected", ...result }; + }, + }; + } + + async importConnectionLink( + url: string, + target: PairingNavigationTarget, + ): Promise { + if (target === "openProject") { + const { offer, password } = parseOfferConnectionUrl(url); + if (!(await this.confirmLink(offer))) return { status: "cancelled" }; + const profile = await this.upsertConnectionFromOffer(offer, undefined, password); + return { status: "connected", serverId: profile.serverId }; + } + const pairing = this.beginLinkPairing(); + try { + const result = await pairing.submit(url); + if (result.status === "cancelled") return result; + return { status: "connected", serverId: result.serverId }; + } catch (error) { + if (!getConnectionAuthFailureReason(error)) throw error; + return { status: "password_required", link: url, pairing }; + } + } + + private async upsertConnectionFromOffer( offer: ConnectionOffer, label?: string, password?: string, ): Promise { - // COMPAT(oldRelayOfferTls): added in v0.1.73, remove after 2026-11-10. - const useTls = offer.relay.useTls ?? shouldUseTlsForDefaultHostedRelay(offer.relay.endpoint); + const connection = relayConnectionFromOffer(offer); return this.upsertRelayConnection({ serverId: offer.serverId, - relayEndpoint: offer.relay.endpoint, - useTls, - daemonPublicKeyB64: offer.daemonPublicKeyB64, + relayEndpoint: connection.relayEndpoint, + useTls: connection.useTls, + daemonPublicKeyB64: connection.daemonPublicKeyB64, label, password, }); } - async upsertConnectionFromOfferUrl( - offerUrlOrFragment: string, - label?: string, - password?: string, - ): Promise { - const parsed = parseOfferConnectionUrl(offerUrlOrFragment); - return this.upsertConnectionFromOffer(parsed.offer, label, password ?? parsed.password); - } - - async probeAndUpsertConnectionFromOfferUrl( - offerUrlOrFragment: string, - password?: string, + private async probeAndUpsertOffer( + offer: ConnectionOffer, + password: string | undefined, ): Promise<{ profile: HostProfile; serverId: string; hostname: string | null }> { - const parsed = parseOfferConnectionUrl(offerUrlOrFragment); - const offer = parsed.offer; - const credential = password ?? parsed.password; - const useTls = offer.relay.useTls ?? shouldUseTlsForDefaultHostedRelay(offer.relay.endpoint); - const relayEndpoint = normalizeHostPort(offer.relay.endpoint); - const connection: HostConnection = { - id: useTls ? `relay:wss:${relayEndpoint}` : `relay:${relayEndpoint}`, - type: "relay", - relayEndpoint, - useTls, - daemonPublicKeyB64: offer.daemonPublicKeyB64, - }; + const connection = relayConnectionFromOffer(offer); const probeHost: HostProfile = { serverId: offer.serverId, - ...(credential ? { password: credential } : {}), + ...(password ? { password } : {}), label: offer.serverId, appearance: defaultHostAppearance(), lifecycle: {}, @@ -1974,27 +2052,10 @@ export class HostRuntimeStore { }; const { client, hostname } = await this.deps.connectToDaemon({ host: probeHost, connection }); await client.close().catch(() => undefined); - const profile = await this.upsertConnectionFromOffer(offer, hostname ?? undefined, credential); + const profile = await this.upsertConnectionFromOffer(offer, hostname ?? undefined, password); return { profile, serverId: offer.serverId, hostname }; } - async importConnectionLink( - url: string, - target: PairingNavigationTarget, - ): Promise<{ status: "connected"; serverId: string } | { status: "password_required" }> { - if (target === "openProject") { - const profile = await this.upsertConnectionFromOfferUrl(url); - return { status: "connected", serverId: profile.serverId }; - } - try { - const { serverId } = await this.probeAndUpsertConnectionFromOfferUrl(url); - return { status: "connected", serverId }; - } catch (error) { - if (!getConnectionAuthFailureReason(error)) throw error; - return { status: "password_required" }; - } - } - async upsertConnectionFromListen(input: { listenAddress: string; serverId: string; @@ -2742,6 +2803,29 @@ export function useHostRegistryLoaded(): boolean { ); } +export function useHostConfirmation(): { + pending: HostConfirmationRequest | null; + answer: (approved: boolean) => void; +} { + const store = getHostRuntimeStore(); + const pending = useSyncExternalStore( + (onStoreChange) => store.subscribeHostConfirmation(onStoreChange), + () => store.getPendingHostConfirmation(), + () => store.getPendingHostConfirmation(), + ); + // The answer is bound to the request this render shows, so a tap never + // answers a request that replaced it. + return useMemo( + () => ({ + pending, + answer: (approved: boolean) => { + if (pending) store.answerHostConfirmation(pending.id, approved); + }, + }), + [pending, store], + ); +} + export interface HostMutations { upsertDirectConnection: (input: { serverId: string; @@ -2762,24 +2846,7 @@ export interface HostMutations { daemonPort?: number; label?: string; }) => Promise<{ profile: HostProfile; serverId: string; hostname: string | null }>; - upsertRelayConnection: (input: { - serverId: string; - relayEndpoint: string; - useTls?: boolean; - daemonPublicKeyB64: string; - label?: string; - password?: string; - }) => Promise; - upsertConnectionFromOffer: (offer: ConnectionOffer, label?: string) => Promise; - upsertConnectionFromOfferUrl: ( - offerUrlOrFragment: string, - label?: string, - password?: string, - ) => Promise; - probeAndUpsertConnectionFromOfferUrl: ( - offerUrlOrFragment: string, - password?: string, - ) => Promise<{ profile: HostProfile; serverId: string; hostname: string | null }>; + beginLinkPairing: () => LinkPairing; renameHost: (serverId: string, label: string) => Promise; setHostColor: (serverId: string, color: HostColor) => Promise; setHostBadgeDisplay: (serverId: string, badgeDisplay: HostBadgeDisplay) => Promise; @@ -2794,12 +2861,7 @@ export function useHostMutations(): HostMutations { upsertDirectConnection: (input) => store.upsertDirectConnection(input), probeAndUpsertDirectConnection: (input) => store.probeAndUpsertDirectConnection(input), probeAndUpsertRemoteSshConnection: (input) => store.probeAndUpsertRemoteSshConnection(input), - upsertRelayConnection: (input) => store.upsertRelayConnection(input), - upsertConnectionFromOffer: (offer, label) => store.upsertConnectionFromOffer(offer, label), - upsertConnectionFromOfferUrl: (url, label, password) => - store.upsertConnectionFromOfferUrl(url, label, password), - probeAndUpsertConnectionFromOfferUrl: (url, password) => - store.probeAndUpsertConnectionFromOfferUrl(url, password), + beginLinkPairing: () => store.beginLinkPairing(), renameHost: (serverId, label) => store.renameHost(serverId, label), setHostColor: (serverId, color) => store.setHostColor(serverId, color), setHostBadgeDisplay: (serverId, badgeDisplay) => diff --git a/packages/app/src/types/host-connection.ts b/packages/app/src/types/host-connection.ts index 4788133e64..0962c02c7c 100644 --- a/packages/app/src/types/host-connection.ts +++ b/packages/app/src/types/host-connection.ts @@ -1,6 +1,8 @@ +import type { ConnectionOffer } from "@getpaseo/protocol/connection-offer"; import { normalizeHostPort, normalizeLoopbackToLocalhost, + shouldUseTlsForDefaultHostedRelay, } from "@getpaseo/protocol/daemon-endpoints"; import { DirectTcpHostConnectionSchema, @@ -354,6 +356,19 @@ export function connectionFromListen(listen: string): HostConnection | null { } } +export function relayConnectionFromOffer(offer: ConnectionOffer): RelayHostConnection { + // COMPAT(oldRelayOfferTls): added in v0.1.73, remove after 2026-11-10. + const useTls = offer.relay.useTls ?? shouldUseTlsForDefaultHostedRelay(offer.relay.endpoint); + const relayEndpoint = normalizeHostPort(offer.relay.endpoint); + return { + id: useTls ? `relay:wss:${relayEndpoint}` : `relay:${relayEndpoint}`, + type: "relay", + relayEndpoint, + useTls, + daemonPublicKeyB64: offer.daemonPublicKeyB64.trim(), + }; +} + export function createRemoteSshHostConnection(input: { host: string; sshPort?: number; From 318a84a5703b41a05792495c348eb9510e4efab6 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Wed, 30 Sep 2026 18:11:38 +0100 Subject: [PATCH 027/100] Keep native dialogs and sheets usable when stacked (#5772) * fix(app): stack adaptive sheets across native presentations Root-owned dialogs could not present over a sibling native dialog on iOS. Use the existing app-wide sheet stack for both native presentation forms, and release the underlying keyboard when a new sheet opens. * test(app): verify pairing sheet autofocus * docs(app): clarify stacked sheet journey prerequisites --- packages/app/e2e/mobile/modal-sheet/README.md | 49 ++++++++++++ .../e2e/mobile/modal-sheet/stacked-pairing.ad | 50 ++++++++++++ .../mobile/modal-sheet/stacked-settings.ad | 46 +++++++++++ .../src/components/adaptive-modal-sheet.tsx | 77 ++++++++++--------- 4 files changed, 184 insertions(+), 38 deletions(-) create mode 100644 packages/app/e2e/mobile/modal-sheet/stacked-pairing.ad create mode 100644 packages/app/e2e/mobile/modal-sheet/stacked-settings.ad diff --git a/packages/app/e2e/mobile/modal-sheet/README.md b/packages/app/e2e/mobile/modal-sheet/README.md index d95034dad4..8a59b16883 100644 --- a/packages/app/e2e/mobile/modal-sheet/README.md +++ b/packages/app/e2e/mobile/modal-sheet/README.md @@ -90,3 +90,52 @@ a failed provider tap leaves its modal open, so dismiss it before the next run. journeys remain outside the default mobile suite, alongside the compact sheet journeys above. Keep generated screenshots, recordings, and run logs outside the repository. + +## Stacked adaptive sheets + +`stacked-pairing.ad` opens the welcome screen's pairing sheet and then the root-mounted host +confirmation. It first types without tapping the input to verify autofocus, then cancels the +confirmation, closes and reopens the underlying sheet, confirms again, +and completes the password retry. `stacked-settings.ad` checks the same return and retry through +Settings → add host → Direct connection → Advanced. Run both on phones and tablets so both the +bottom sheet and centered dialog are exercised. + +These integration journeys require the host-confirmation flow in the test app. Include both the +sheet fix and that flow in the QA checkout when verifying them; the production sheet component +has no dependency on the host-confirmation feature. + +Use a fresh English app with notification permission granted and first-use keyboard prompts +already dismissed. Load this checkout from an isolated Metro. Start an isolated password-protected +daemon and a local Wrangler relay with `--var 'PASEO_RELAY_UPSTREAM:'`; the checked-in upstream +setting otherwise forwards traffic to the hosted relay, which drops password rejection replies. +Set `PAIRING_LINK` to the daemon's `relay://` URI with an intentionally incorrect `password` query +parameter, and `PASSWORD` to its correct password. This exercises password retry even while the +daemon accepts relay connections with no credential. + +```sh +agent-device replay packages/app/e2e/mobile/modal-sheet/stacked-pairing.ad \ + --state-dir "$SHEET_QA_DEVICE_STATE" --session stacked-ipad --platform ios \ + --udid "$SHEET_QA_IOS_UDID" \ + --env PAIRING_LINK="$SHEET_QA_LINK" --env PASSWORD="$SHEET_QA_PASSWORD" \ + --env OUT="$SHEET_QA_OUTPUT" +``` + +For Settings, first open Settings with Add host visible. With a saved host, open Switch host +first. Use a second loopback spelling +(`localhost` instead of `127.0.0.1`) for the same local relay so the saved host asks for confirmation +of its changed connection. Pass that link to `stacked-settings.ad` with the same arguments. +For an app with no saved hosts, use the original link; Add host is visible directly in Settings. + +Android uses the same journeys: copy each script outside the repository, change its context's +`platform=ios` to `platform=android`, and pass `--serial` instead of `--udid`. Reverse both Metro +(including the port advertised by its manifest) and the local relay ports. Keep driver state, +artifacts, and sessions separate from other runs. These host-dependent journeys remain outside the +default mobile suite, alongside the existing modal-sheet scripts. + +On iPhone, bulk replacement or bulk keyboard entry of the advanced URI field triggers a +repeated-update error with both the parent and stacked-sheet implementations. Entering the complete +URI one character at a time passed with both implementations; faster typing remains unverified. +On iPad, replacing the field a second time appends text. For this Settings journey, use the +native clipboard Paste menu, then continue the assertions manually. Dismiss the password keyboard +with Done to submit when the Connect button is below the keyboard. The welcome pairing regression +runs end to end without these adjustments. diff --git a/packages/app/e2e/mobile/modal-sheet/stacked-pairing.ad b/packages/app/e2e/mobile/modal-sheet/stacked-pairing.ad new file mode 100644 index 0000000000..d51d3ce060 --- /dev/null +++ b/packages/app/e2e/mobile/modal-sheet/stacked-pairing.ad @@ -0,0 +1,50 @@ +# Fresh English app at Welcome, local relay host with PASSWORD, see README.md. +# Run on phones and tablets: the confirmation lives at the root, outside the pairing sheet. +context platform=ios timeout=180000 retries=0 +env APP_ID=sh.paseo.debug + +open "${APP_ID}" +wait "id=\"welcome-paste-pairing-link\"" 20000 +screenshot "${OUT}/01-welcome.png" +press "id=\"welcome-paste-pairing-link\"" +wait stable +# Type without tapping the input: opening the sheet must retain its autofocus. +wait "id=\"pair-link-input\"" 10000 +type "relay://" +wait "id=\"pair-link-input\" value=\"relay://\"" 5000 +screenshot "${OUT}/20-pairing-autofocus-without-tap.png" +fill "id=\"pair-link-input\"" "${PAIRING_LINK}" +wait stable +screenshot "${OUT}/02-pairing-input.png" +press "id=\"pair-link-submit\"" +wait stable +wait "id=\"host-confirmation-connect\"" 10000 +screenshot "${OUT}/03-confirmation-on-top.png" +press "id=\"host-confirmation-cancel\"" +wait stable +wait "id=\"pair-link-submit\" enabled=true" 10000 +screenshot "${OUT}/04-cancel-returns-to-pairing.png" +# Close and reopen the underlying sheet, then edit it and submit again. +press "id=\"pair-link-cancel\"" +wait stable +wait "id=\"welcome-paste-pairing-link\"" 10000 +screenshot "${OUT}/05-underlying-sheet-closes.png" +press "id=\"welcome-paste-pairing-link\"" +wait stable +wait "id=\"pair-link-input\"" 10000 +fill "id=\"pair-link-input\"" "${PAIRING_LINK}" +wait stable +press "id=\"pair-link-submit\"" +wait stable +wait "id=\"host-confirmation-connect\"" 10000 +screenshot "${OUT}/06-confirmation-reopens.png" +press "id=\"host-confirmation-connect\"" +wait stable +wait "id=\"pair-link-password-input\"" 20000 +screenshot "${OUT}/07-password-after-connect.png" +fill "id=\"pair-link-password-input\"" "${PASSWORD}" +wait stable +press "id=\"pair-link-submit\"" +wait stable +wait text "Open a folder on your machine" 30000 +screenshot "${OUT}/08-connected.png" diff --git a/packages/app/e2e/mobile/modal-sheet/stacked-settings.ad b/packages/app/e2e/mobile/modal-sheet/stacked-settings.ad new file mode 100644 index 0000000000..da687d7e02 --- /dev/null +++ b/packages/app/e2e/mobile/modal-sheet/stacked-settings.ad @@ -0,0 +1,46 @@ +# Settings with Add host visible: no saved hosts, or the host picker already open. +context platform=ios timeout=180000 retries=0 +env APP_ID=sh.paseo.debug +open "${APP_ID}" +wait "id=\"settings-add-host\"" 20000 +screenshot "${OUT}/09-settings.png" +press "id=\"settings-add-host\"" +wait stable +wait "id=\"add-host-method-direct\"" 10000 +screenshot "${OUT}/10-add-host-methods.png" +press "id=\"add-host-method-direct\"" +wait stable +press "id=\"direct-host-advanced-toggle\"" +wait stable +fill "id=\"direct-host-uri-input\"" "${PAIRING_LINK}" +wait stable +screenshot "${OUT}/11-add-host-relay-input.png" +press "id=\"direct-host-submit\"" +wait stable +wait "id=\"host-confirmation-connect\"" 10000 +screenshot "${OUT}/12-add-host-confirmation-on-top.png" +press "id=\"host-confirmation-cancel\"" +wait stable +wait "id=\"direct-host-submit\" enabled=true" 10000 +screenshot "${OUT}/13-cancel-returns-to-add-host.png" +# Editing the underlying sheet proves it still accepts input after closing the top sheet. +fill "id=\"direct-host-uri-input\"" "${PAIRING_LINK}" +wait stable +press "id=\"direct-host-submit\"" +wait stable +wait "id=\"host-confirmation-connect\"" 10000 +screenshot "${OUT}/14-add-host-confirmation-reopens.png" +press "id=\"host-confirmation-connect\"" +wait stable +wait "id=\"direct-password-input\"" 20000 +screenshot "${OUT}/15-add-host-password.png" +fill "id=\"direct-password-input\"" "${PASSWORD}" +wait stable +press "id=\"direct-host-submit\"" +wait stable +open "${APP_ID}" "paseo://settings" +wait "label=\"Connections\"" 10000 +press "label=\"Connections\"" +wait stable +wait "id=\"host-page-connections-card\"" 30000 +screenshot "${OUT}/16-settings-connected.png" diff --git a/packages/app/src/components/adaptive-modal-sheet.tsx b/packages/app/src/components/adaptive-modal-sheet.tsx index 2205409db1..78cdfd2e7a 100644 --- a/packages/app/src/components/adaptive-modal-sheet.tsx +++ b/packages/app/src/components/adaptive-modal-sheet.tsx @@ -1,8 +1,8 @@ -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import type { ReactNode } from "react"; import { createPortal } from "react-dom"; import { useTranslation } from "react-i18next"; -import { Modal, Platform, Pressable, Text, View } from "react-native"; +import { Keyboard, Pressable, Text, View } from "react-native"; import type { DimensionValue, StyleProp, ViewStyle } from "react-native"; import { StyleSheet, useUnistyles } from "react-native-unistyles"; import { useIsCompactFormFactor } from "@/constants/layout"; @@ -19,7 +19,6 @@ import { type BottomSheetBackgroundProps, } from "@gorhom/bottom-sheet"; import Animated, { useAnimatedStyle } from "react-native-reanimated"; -import { GestureHandlerRootView } from "react-native-gesture-handler"; import { ArrowLeft, Search, X } from "lucide-react-native"; import { IsolatedBottomSheetModal, @@ -75,11 +74,15 @@ export interface SheetHeader { const SCROLL_CONTENT_GROW = { flexGrow: 1 }; const ABSOLUTE_FILL_STYLE = { ...StyleSheet.absoluteFillObject }; +const NATIVE_DIALOG_SNAP_POINTS = ["100%"]; const styles = StyleSheet.create((theme) => ({ - nativeModalRoot: { + nativeDialogSurface: { flex: 1, }, + nativeDialogBackground: { + backgroundColor: "transparent", + }, desktopOverlay: { ...StyleSheet.absoluteFillObject, backgroundColor: "rgba(0,0,0,0.55)", @@ -471,7 +474,7 @@ export interface AdaptiveModalSheetProps { contentStyle?: StyleProp; /** Size compact sheet content to the live snap height instead of its largest snap point. */ sizeContentToCurrentSnapPoint?: boolean; - /** Re-establishes caller-owned contexts inside the compact bottom-sheet portal. */ + /** Re-establishes caller-owned contexts inside the native or compact sheet portal. */ contextBridge?: ContextBridge | null; } @@ -526,26 +529,26 @@ export function AdaptiveModalSheet({ () => ({ backgroundColor: theme.colors.palette.zinc[600] }), [theme.colors.palette.zinc], ); + useEffect(() => { + if (!isWeb && visible) { + // A newly opened sheet owns input. A keyboard belonging to the sheet below + // would cover controls in the new sheet, which has no focused input yet. + Keyboard.dismiss(); + } + }, [visible]); + const { sheetRef, handleSheetChange, handleSheetDismiss } = useIsolatedBottomSheetVisibility({ visible, - isEnabled: isMobile, + isEnabled: isMobile || !isWeb, onClose, }); const [shouldRenderWeb, setShouldRenderWeb] = useState(visible); const [isWebClosing, setIsWebClosing] = useState(false); const modalLayer = useGlobalWebOverlayLayer("modal", isWeb && !isMobile && shouldRenderWeb); - const nativeModalDismissNotifiedRef = useRef(!visible); const handleDismiss = useCallback(() => { handleSheetDismiss(); onDismiss?.(); }, [handleSheetDismiss, onDismiss]); - const notifyNativeModalDismiss = useCallback(() => { - if (nativeModalDismissNotifiedRef.current) { - return; - } - nativeModalDismissNotifiedRef.current = true; - onDismiss?.(); - }, [onDismiss]); const renderBackdrop = useCallback( (props: React.ComponentProps) => ( @@ -592,12 +595,6 @@ export function AdaptiveModalSheet({ onKeyDown: handleWebOverlayKeyDown, }); - useEffect(() => { - if (visible) { - nativeModalDismissNotifiedRef.current = false; - } - }, [visible]); - useEffect(() => { if (!isWeb || isMobile) return; if (visible) { @@ -615,12 +612,6 @@ export function AdaptiveModalSheet({ return () => window.clearTimeout(timeout); }, [visible, isMobile, onDismiss, shouldRenderWeb]); - useEffect(() => { - if (isWeb || isMobile || visible || Platform.OS !== "android") return; - const timeout = setTimeout(notifyNativeModalDismiss, 0); - return () => clearTimeout(timeout); - }, [visible, isMobile, notifyNativeModalDismiss]); - if (isMobile) { const sheetContent = ( <> @@ -725,18 +716,28 @@ export function AdaptiveModalSheet({ } return ( - - {/* Android Modal opens a separate window outside the app's gesture root. */} - - {desktopContent} - - + {desktopContent} + ); } From 29c198f9554679f9f8cfde49f63d069bbb66d4da Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Wed, 30 Sep 2026 19:37:14 +0100 Subject: [PATCH 028/100] fix(providers): apply configured options to every agent provider (#5780) * fix(providers): apply configured options at session startup Treat provider options as one opaque record, with provider-owned validation and per-agent overrides over configured defaults. Keep agent overrides separate from defaults so configuration changes apply on the next launch or resume. * test(providers): verify provider-owned validation through Hub * fix(providers): preserve the complete wrapped session interface --- docs/custom-providers.md | 47 ++++++-- docs/plugins.md | 6 +- docs/providers.md | 48 ++------ .../browser/agent-message-submission.spec.ts | 1 + packages/plugin/src/server/provider.ts | 4 +- packages/protocol/src/agent-types.ts | 2 +- packages/protocol/src/messages.ts | 2 +- packages/protocol/src/provider-config.ts | 2 + packages/protocol/src/schedule/types.ts | 2 +- .../agent/agent-configuration-validator.ts | 15 --- .../server/src/server/agent/agent-manager.ts | 22 +--- .../src/server/agent/agent-sdk-types.ts | 6 +- .../server/src/server/agent/agent-storage.ts | 2 +- .../src/server/agent/plugin-provider.test.ts | 107 ++++++++++++++++ .../src/server/agent/plugin-provider.ts | 9 +- .../src/server/agent/provider-options.test.ts | 67 ++++++++++ .../src/server/agent/provider-options.ts | 34 +++++- .../agent/provider-registry-wrap.test.ts | 56 ++++++++- .../server/agent/provider-registry.test.ts | 86 ++----------- .../src/server/agent/provider-registry.ts | 114 +++++++++--------- .../agent/provider-snapshot-manager.test.ts | 18 ++- .../server/agent/provider-snapshot-manager.ts | 9 +- .../src/server/agent/providers/acp-agent.ts | 34 ++++-- .../src/server/agent/providers/acp-options.ts | 18 +++ .../server/agent/providers/claude/agent.ts | 7 +- .../providers/codex-app-server-agent.test.ts | 17 +++ .../agent/providers/codex-app-server-agent.ts | 5 +- .../agent/providers/cursor-acp-agent.ts | 2 - .../generic-acp-agent.diagnostic.test.ts | 27 +++-- .../agent/providers/generic-acp-agent.test.ts | 17 --- .../agent/providers/generic-acp-agent.ts | 37 +----- .../server/agent/providers/kimi-acp-agent.ts | 2 - .../server/agent/providers/kiro-acp-agent.ts | 2 - .../server/agent/providers/omp/agent.test.ts | 6 +- .../src/server/agent/providers/omp/agent.ts | 64 +++++----- .../agent/providers/omp/provider-config.ts | 40 +++--- .../server/agent/providers/opencode-agent.ts | 5 +- .../server/agent/providers/pi/agent.test.ts | 16 +-- .../src/server/agent/providers/pi/agent.ts | 48 +++++--- .../server/agent/providers/trae-acp-agent.ts | 2 - .../src/server/hub/daemon-executions.test.ts | 30 ++++- .../src/server/schedule/service.test.ts | 8 -- public-docs/custom-providers.md | 21 +++- public-docs/plugins/reference.md | 7 +- public-docs/sdk/provider-options.md | 41 ++++++- 45 files changed, 690 insertions(+), 425 deletions(-) create mode 100644 packages/server/src/server/agent/providers/acp-options.ts diff --git a/docs/custom-providers.md b/docs/custom-providers.md index 1f872b9d1b..efc5fdf24f 100644 --- a/docs/custom-providers.md +++ b/docs/custom-providers.md @@ -31,6 +31,35 @@ agent catalog during startup, raise the limit in milliseconds: The limit applies independently to every provider refresh and covers availability plus the entire catalog probe. `PASEO_PROVIDER_REFRESH_TIMEOUT_MS` sets it when the config field is absent. +## Provider options + +Set provider defaults in `agents.providers..options`. At agent creation, use +`providerOptions` on the wire or `config.options` in the SDK. Both are opaque +`Record` values: the provider owns validation and application. +Claude, Codex, and OpenCode reject unknown keys with a key-specific creation error. +Providers without option handling ignore the record. + +The daemon deep-merges the agent's options over the provider defaults at each +session launch or resume. Plain objects merge recursively; arrays, scalars, and +`null` replace the base value. Only the agent's own options are persisted, so edits +to provider defaults apply on the next launch or resume. A profile inherits its +base provider's options unless it sets its own options record. + +```json +{ + "agents": { + "providers": { + "codex": { + "options": { "sandbox_mode": "read-only", "approval_policy": "never" } + } + } + } +} +``` + +See the [SDK provider options guide](../public-docs/sdk/provider-options.md) for +per-agent examples and provider-specific keys. + --- ## Table of Contents @@ -391,7 +420,7 @@ Custom OMP profiles should extend `omp`. They inherit the OMP adapter's `rpc-ui` "XDG_CONFIG_HOME": "~/.config/omp-work", "XDG_STATE_HOME": "~/.local/state/omp-work" }, - "params": { + "options": { "sessionDir": "~/.local/state/omp-work/omp/agent/sessions", "rpcTimeoutMs": 60000, "smolModel": "openai/gpt-5-mini", @@ -404,7 +433,7 @@ Custom OMP profiles should extend `omp`. They inherit the OMP adapter's `rpc-ui` } ``` -`params.sessionDir` is used only for importing sessions that were started outside Paseo. If `command` or XDG env vars move OMP's state directory, set `params.sessionDir` to the resulting OMP JSONL session directory; launching and resuming still go through the configured command. OMP waits 20 seconds for its initial `ready` frame and 60 seconds for later control-plane RPCs by default. `params.rpcTimeoutMs` overrides both deadlines. +`options.sessionDir` is used only for importing sessions that were started outside Paseo. If `command` or XDG env vars move OMP's state directory, set `options.sessionDir` to the resulting OMP JSONL session directory; launching and resuming still go through the configured command. OMP waits 20 seconds for its initial `ready` frame and 60 seconds for later control-plane RPCs by default. `options.rpcTimeoutMs` overrides both deadlines. For other providers that keep Pi's `--mode rpc` API but write sessions somewhere else, extend `pi`, replace the command, and provide the JSONL session directory: @@ -416,7 +445,7 @@ For other providers that keep Pi's `--mode rpc` API but write sessions somewhere "extends": "pi", "label": "My Pi Fork", "command": ["my-pi-fork"], - "params": { + "options": { "sessionDir": "~/.my-pi-fork/sessions", "rpcTimeoutMs": 60000 } @@ -426,7 +455,7 @@ For other providers that keep Pi's `--mode rpc` API but write sessions somewhere } ``` -This session directory is also import-only. Launching and resuming still go through the configured command, so this example resumes with `my-pi-fork --mode rpc --session `. `params.rpcTimeoutMs` overrides the 60-second Pi control-plane RPC deadline. +This session directory is also import-only. Launching and resuming still go through the configured command, so this example resumes with `my-pi-fork --mode rpc --session `. `options.rpcTimeoutMs` overrides the 60-second Pi control-plane RPC deadline. --- @@ -484,7 +513,7 @@ Required fields for ACP providers: - `label` - `command` — the command to spawn the agent process (must support ACP over stdio) -Paseo tools such as subagent creation come from the shared internal tool catalog. ACP providers receive those tools through the MCP fallback by default because ACP exposes `mcpServers`, not Paseo's native tool catalog. Some ACP adapters cannot create sessions when `mcpServers` is non-empty. Disable injected MCP for those providers with `params.supportsMcpServers: false`: +Paseo tools such as subagent creation come from the shared internal tool catalog. ACP providers receive those tools through the MCP fallback by default because ACP exposes `mcpServers`, not Paseo's native tool catalog. Some ACP adapters cannot create sessions when `mcpServers` is non-empty. Disable injected MCP for those providers with `options.supportsMcpServers: false`: ```json { @@ -494,7 +523,7 @@ Paseo tools such as subagent creation come from the shared internal tool catalog "extends": "acp", "label": "My Agent", "command": ["my-agent", "acp"], - "params": { + "options": { "supportsMcpServers": false } } @@ -505,7 +534,7 @@ Paseo tools such as subagent creation come from the shared internal tool catalog ACP agents execute filesystem operations in their own environment by default, while terminal operations run through Paseo on the host. To customize which -operations Paseo handles, configure client capabilities in provider params: +operations Paseo handles, configure client capabilities in provider options: ```json { @@ -515,7 +544,7 @@ operations Paseo handles, configure client capabilities in provider params: "extends": "acp", "label": "Container Agent", "command": ["container-agent", "acp"], - "params": { + "options": { "clientCapabilities": { "fs": { "readTextFile": false, @@ -684,7 +713,7 @@ Every entry under `agents.providers` accepts these fields: | `description` | `string` | No | Short description shown in the UI | | `command` | `string[]` | Yes (ACP only) | Command to spawn the agent process | | `env` | `Record` | No | Environment variables to set for the agent process | -| `params` | `Record` | No | Provider-specific options such as `supportsMcpServers: false` | +| `options` | `Record` | No | Provider-specific options such as `supportsMcpServers: false` | | `models` | `ProviderProfileModel[]` | No | Static model list (overrides runtime discovery) | | `additionalModels` | `ProviderProfileModel[]` | No | Static model additions (merged with runtime discovery or `models`) | | `disallowedTools` | `string[]` | No | Tool names to disable for this provider (e.g. `["WebSearch"]`) | diff --git a/docs/plugins.md b/docs/plugins.md index a354a20374..a739d417a0 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -431,7 +431,11 @@ need no change. See [catalogue ownership](providers.md#provider-snapshot-refresh configuration, permissions, persistence, and complete timeline snapshots through `onEvent()`. Route messages, structured commands, steering, and command side effects through `session.prompt`. Provider settings are toggle/select data that Paseo renders in the composer. Keep private options in -the opaque `providerOptions` config object. +the opaque `ProviderSessionConfig.providerOptions` object on `session.open`. +It contains the provider defaults and per-agent overrides merged by the daemon. +Validate and apply it inside the provider; core does not know your option shape. +See [provider options](custom-providers.md#provider-options) for configuration and +merge semantics. Agent refresh closes the current provider session and opens it again with current configuration and persistence. Re-read credentials and provider-owned configuration on `session.open`; consume the diff --git a/docs/providers.md b/docs/providers.md index 6373caa57c..0036c2e6f2 100644 --- a/docs/providers.md +++ b/docs/providers.md @@ -22,47 +22,15 @@ provider icon map. You do not need a core manifest entry or provider factory. ## Provider-native session options -`AgentSessionConfig.providerOptions` carries JSON-safe configuration for the selected provider. The -names and nesting are the provider's native contract; options are not portable between providers. -Paseo validates the object with the selected provider's strict schema before constructing a session. -Unknown keys fail with their `providerOptions.*` path. Paseo-owned controls such as cwd, model, -prompt, environment, session identity, MCP transport, callbacks, and hooks are not accepted as -top-level provider options. +The provider owns validation and application of the opaque record in +`AgentSessionConfig.providerOptions`. The registry supplies the effective options +at session startup. See [provider configuration](custom-providers.md#provider-options) +for defaults and merge rules, and the [SDK guide](../public-docs/sdk/provider-options.md) +for native keys and examples. -This Paseo version accepts these keys: - -- **Codex:** `approval_policy`, `sandbox_mode`, - `sandbox_workspace_write.{writable_roots,network_access,exclude_slash_tmp,exclude_tmpdir_env_var}`, - `web_search`, `features.multi_agent_v2`, and `features.network_proxy`. A network proxy object may - contain `enabled`, `proxy_url`, `socks_url`, `enable_socks5`, `enable_socks5_udp`, - `allow_local_binding`, `allow_upstream_proxy`, `dangerously_allow_all_unix_sockets`, - `dangerously_allow_non_loopback_proxy`, `domains`, and `unix_sockets`. See the - [Codex configuration reference](https://developers.openai.com/codex/config-reference). -- **Claude:** `allowedTools`, `disallowedTools`, `additionalDirectories`, `extraArgs`, `sandbox`, and - `settings`. `providerOptions.extraArgs` passes the SDK's documented - [`Options.extraArgs`](https://platform.claude.com/docs/en/agent-sdk/typescript#options) map - unchanged: keys omit the leading `--`, string values supply an argument value, and `null` - supplies a boolean flag. For example, `providerOptions: { extraArgs: { chrome: null } }` - passes `--chrome`, and `providerOptions: { extraArgs: { model: "x" } }` passes `--model x`. - Set it in session configuration or a plugin's `server.before("agent.create", ...)` hook; see - [plugin configuration hooks](../public-docs/plugins/reference.md#change-configuration-and-inject-an-mcp-server). Values are literal; - shell expressions such as `$(command)` are not evaluated. The accepted sandbox - fields cover enablement, fail-if-unavailable behavior, excluded and unsandboxed commands, - filesystem read/write rules, network domain/socket/local-binding rules, weaker nested - sandboxing, ignored violations, and the ripgrep command. `settings` accepts native - `permissions.{allow,ask,deny}` and sandbox settings. See the - [Claude Agent SDK TypeScript reference](https://platform.claude.com/docs/en/agent-sdk/typescript) - and [Claude settings reference](https://code.claude.com/docs/en/settings). -- **OpenCode:** `permission`, either one `ask`/`allow`/`deny` action or the native per-tool rule - object. Supported entries are `read`, `edit`, `glob`, `grep`, `list`, `bash`, `task`, - `external_directory`, `todowrite`, `question`, `webfetch`, `websearch`, `codesearch`, - `repo_clone`, `repo_overview`, `lsp`, `doom_loop`, and `skill`. See the - [OpenCode permissions reference](https://opencode.ai/docs/permissions/). OpenCode permissions are - application policy, not an OS sandbox. - -Each provider definition owns its option schema and exact MCP preapproval mapping. A new provider -must fail closed for Hub unattended execution until it can approve one exact injected MCP server -and tool identity without approving native tools. +Exact MCP preapproval is a separate daemon-owned contract. A new provider must fail +closed for Hub unattended execution until it can approve one exact injected MCP +server and tool identity without approving native tools. ## Core adapter patterns diff --git a/packages/app/e2e/browser/agent-message-submission.spec.ts b/packages/app/e2e/browser/agent-message-submission.spec.ts index e7f87ed103..75cf7ff0c9 100644 --- a/packages/app/e2e/browser/agent-message-submission.spec.ts +++ b/packages/app/e2e/browser/agent-message-submission.spec.ts @@ -1242,6 +1242,7 @@ test.describe("Agent message submission", () => { await gate.waitForHeldServerMessage("send_agent_message_response"); await expect(page.getByText("hello", { exact: true })).toHaveCount(1); await expect(page.getByText(/^Worked for/)).toHaveCount(0); + await expectAgentReadyToInterrupt(page); gate.releaseHeldServerMessage("send_agent_message_response"); await expect(page.getByText("hello", { exact: true })).toHaveCount(1); } finally { diff --git a/packages/plugin/src/server/provider.ts b/packages/plugin/src/server/provider.ts index 772d0d3eac..33b49ab447 100644 --- a/packages/plugin/src/server/provider.ts +++ b/packages/plugin/src/server/provider.ts @@ -114,7 +114,7 @@ export interface ProviderSessionConfig { mode?: string; thinkingOption?: string; settings: Readonly>; - providerOptions?: Readonly>; + providerOptions?: Readonly>; title?: string; persist: boolean; } @@ -742,7 +742,7 @@ const sessionConfigSchema = z mode: z.string().optional(), thinkingOption: z.string().optional(), settings: jsonObjectSchema, - providerOptions: jsonObjectSchema.optional(), + providerOptions: z.record(z.string(), z.unknown()).optional(), title: z.string().optional(), persist: z.boolean(), }) diff --git a/packages/protocol/src/agent-types.ts b/packages/protocol/src/agent-types.ts index f86dc63575..5c0fce9608 100644 --- a/packages/protocol/src/agent-types.ts +++ b/packages/protocol/src/agent-types.ts @@ -498,7 +498,7 @@ export type JsonValue = | JsonValue[] | { [key: string]: JsonValue }; -export type ProviderOptions = Record; +export type ProviderOptions = Record; export interface McpToolRef { kind: "mcp"; diff --git a/packages/protocol/src/messages.ts b/packages/protocol/src/messages.ts index 6549ca3aa6..69595c9ad6 100644 --- a/packages/protocol/src/messages.ts +++ b/packages/protocol/src/messages.ts @@ -418,7 +418,7 @@ const McpServerConfigSchema = z.discriminatedUnion("type", [ McpSseServerConfigSchema, ]); -const ProviderOptionsSchema = z.record(z.string(), z.json()); +const ProviderOptionsSchema = z.record(z.string(), z.unknown()); const McpToolRefSchema = z .object({ diff --git a/packages/protocol/src/provider-config.ts b/packages/protocol/src/provider-config.ts index 7687c813c6..c94f8ca965 100644 --- a/packages/protocol/src/provider-config.ts +++ b/packages/protocol/src/provider-config.ts @@ -54,6 +54,8 @@ export const ProviderOverrideSchema = z.object({ description: z.string().optional(), command: z.array(z.string().min(1)).min(1).optional(), env: z.record(z.string(), z.string()).optional(), + options: z.record(z.string(), z.unknown()).optional(), + // COMPAT(providerParams): added in v0.10.0, remove after 2027-03-30 once configs use options. params: z.record(z.string(), z.unknown()).optional(), models: z.array(ProviderProfileModelSchema).optional(), additionalModels: z.array(ProviderProfileModelSchema).optional(), diff --git a/packages/protocol/src/schedule/types.ts b/packages/protocol/src/schedule/types.ts index 62c02bd38d..718c7e05ca 100644 --- a/packages/protocol/src/schedule/types.ts +++ b/packages/protocol/src/schedule/types.ts @@ -33,7 +33,7 @@ export const ScheduleTargetSchema = z.discriminatedUnion("type", [ archiveOnFinish: z.boolean().optional(), isolation: z.enum(["local", "worktree"]).optional(), title: z.string().trim().min(1).nullable().optional(), - providerOptions: z.record(z.string(), z.json()).optional(), + providerOptions: z.record(z.string(), z.unknown()).optional(), featureValues: z.record(z.string(), z.unknown()).optional(), systemPrompt: z.string().optional(), mcpServers: z.record(z.string(), z.unknown()).optional(), diff --git a/packages/server/src/server/agent/agent-configuration-validator.ts b/packages/server/src/server/agent/agent-configuration-validator.ts index 1135d25e0b..0893f3a692 100644 --- a/packages/server/src/server/agent/agent-configuration-validator.ts +++ b/packages/server/src/server/agent/agent-configuration-validator.ts @@ -3,7 +3,6 @@ import type { HubExecutionAgentValidationIssue } from "@getpaseo/protocol/messag import type { ProviderSnapshotEntry } from "./agent-sdk-types.js"; import { filterSelectableAgentModels } from "./agent-sdk-types.js"; -import { ProviderOptionsValidationError } from "./provider-options.js"; export interface AgentConfigurationValidationInput { provider: string; @@ -16,13 +15,11 @@ export interface AgentConfigurationValidationInput { interface AgentConfigurationValidationContext { input: AgentConfigurationValidationInput; provider: ProviderSnapshotEntry; - validateOptions(options: ProviderOptions | undefined): ProviderOptions | undefined; } export function validateAgentConfigurationAgainstProvider({ input, provider, - validateOptions, }: AgentConfigurationValidationContext): HubExecutionAgentValidationIssue[] { const issues: HubExecutionAgentValidationIssue[] = []; const models = filterSelectableAgentModels(provider.models); @@ -53,17 +50,5 @@ export function validateAgentConfigurationAgainstProvider({ }); } - try { - validateOptions(input.providerOptions); - } catch (error) { - if (!(error instanceof ProviderOptionsValidationError)) throw error; - issues.push( - ...error.issues.map((issue) => ({ - path: ["providerOptions", ...issue.path], - message: issue.message, - })), - ); - } - return issues; } diff --git a/packages/server/src/server/agent/agent-manager.ts b/packages/server/src/server/agent/agent-manager.ts index 87d3b290aa..aebede5fa1 100644 --- a/packages/server/src/server/agent/agent-manager.ts +++ b/packages/server/src/server/agent/agent-manager.ts @@ -17,7 +17,7 @@ import { PARENT_AGENT_ID_LABEL, } from "@getpaseo/protocol/agent-labels"; import type { Logger } from "pino"; -import type { ProviderOptions, ToolPolicy } from "@getpaseo/protocol/agent-types"; +import type { ToolPolicy } from "@getpaseo/protocol/agent-types"; import type { ProviderPaseoToolsPolicy } from "@getpaseo/protocol/provider-config"; import { z } from "zod"; import type { TerminalManager } from "../../terminal/terminal-manager.js"; @@ -292,11 +292,6 @@ interface AgentManagerRescueTimeouts { interface ProviderEnabledFlag { enabled: boolean; derivedFromProviderId?: string | null; - validateOptions?: (options: ProviderOptions | undefined) => ProviderOptions | undefined; - applyOptions?: ( - config: AgentSessionConfig, - options: ProviderOptions | undefined, - ) => AgentSessionConfig; applyToolPolicy?: ( config: AgentSessionConfig, toolPolicy: ToolPolicy | undefined, @@ -5094,22 +5089,15 @@ export class AgentManager { private applyProviderConfiguration(config: AgentSessionConfig): AgentSessionConfig { const definition = this.providerDefinitions.get(config.provider); - if (config.providerOptions !== undefined && !definition?.validateOptions) { - throw new Error(`Provider '${config.provider}' does not accept providerOptions`); - } - const validatedOptions = definition?.validateOptions?.(config.providerOptions); - const withOptions = definition?.applyOptions - ? definition.applyOptions(config, validatedOptions) - : config; - this.validateToolPolicyServers(withOptions); - if (withOptions.toolPolicy && !definition?.applyToolPolicy) { + this.validateToolPolicyServers(config); + if (config.toolPolicy && !definition?.applyToolPolicy) { throw new Error( `Provider '${config.provider}' cannot preapprove exact MCP tools for unattended execution`, ); } return definition?.applyToolPolicy - ? definition.applyToolPolicy(withOptions, withOptions.toolPolicy) - : withOptions; + ? definition.applyToolPolicy(config, config.toolPolicy) + : config; } private validateToolPolicyServers(config: AgentSessionConfig): void { diff --git a/packages/server/src/server/agent/agent-sdk-types.ts b/packages/server/src/server/agent/agent-sdk-types.ts index f2b42525e9..a0ed0eedc3 100644 --- a/packages/server/src/server/agent/agent-sdk-types.ts +++ b/packages/server/src/server/agent/agent-sdk-types.ts @@ -535,6 +535,7 @@ export interface AgentSlashCommand { } export interface ListImportableSessionsOptions { + providerOptions?: ProviderOptions; limit?: number; /** Optional case-insensitive descriptor search text. */ query?: string; @@ -697,7 +698,7 @@ export interface AgentSession { } | null; } -export type FetchCatalogOptions = +export type FetchCatalogOptions = { providerOptions?: ProviderOptions } & ( | { scope: "global"; force: boolean; @@ -706,7 +707,8 @@ export type FetchCatalogOptions = scope: "workspace"; cwd: string; force: boolean; - }; + } +); export interface ProviderRefreshContext { readonly signal: AbortSignal; diff --git a/packages/server/src/server/agent/agent-storage.ts b/packages/server/src/server/agent/agent-storage.ts index b41bc75cfc..be94a31d49 100644 --- a/packages/server/src/server/agent/agent-storage.ts +++ b/packages/server/src/server/agent/agent-storage.ts @@ -16,7 +16,7 @@ const SERIALIZABLE_CONFIG_SCHEMA = z model: z.string().nullable().optional(), thinkingOptionId: z.string().nullable().optional(), featureValues: z.record(z.string(), z.unknown()).nullable().optional(), - providerOptions: z.record(z.string(), z.json()).nullable().optional(), + providerOptions: z.record(z.string(), z.unknown()).nullable().optional(), toolPolicy: z .object({ preapproved: z.array( diff --git a/packages/server/src/server/agent/plugin-provider.test.ts b/packages/server/src/server/agent/plugin-provider.test.ts index e1976e22a0..da416ee7e1 100644 --- a/packages/server/src/server/agent/plugin-provider.test.ts +++ b/packages/server/src/server/agent/plugin-provider.test.ts @@ -10,6 +10,10 @@ import { setImmediate as nextTurn } from "node:timers/promises"; import { describe, expect, test } from "vitest"; import { createTestLogger } from "../../test-utils/test-logger.js"; import type { AgentClient, AgentStreamEvent } from "./agent-sdk-types.js"; +import { toStoredAgentRecord } from "./agent-projections.js"; +import { AgentManager } from "./agent-manager.js"; +import { buildProviderRegistry } from "./provider-registry.js"; +import { ProviderOverrideSchema } from "@getpaseo/protocol/provider-config"; import { PluginAgentClientRegistry } from "./plugin-provider.js"; import { isStaleProviderSessionError, @@ -283,6 +287,109 @@ function expectNestedChildren(events: AgentStreamEvent[]) { } describe("PluginAgentClientRegistry", () => { + test("stores only agent options while the plugin receives merged defaults", async () => { + const logger = createTestLogger(); + const harness = createProviderHarness(); + const plugins = new PluginAgentClientRegistry(logger); + plugins.replace([harness.registration]); + const registry = buildProviderRegistry(logger, { + pluginProviders: plugins.definitions(), + providerOverrides: { + "plugin-direct": { options: { nested: { base: true, replace: "config" } } }, + }, + }); + const manager = new AgentManager({ + logger, + clients: { "plugin-direct": registry["plugin-direct"].createClient(logger) }, + providerDefinitions: { "plugin-direct": registry["plugin-direct"] }, + }); + const own = { nested: { replace: "agent" } }; + const agent = await manager.createAgent( + { provider: "plugin-direct", cwd: "/tmp", providerOptions: own }, + undefined, + { workspaceId: undefined }, + ); + expect(toStoredAgentRecord(agent).config?.providerOptions).toEqual(own); + expect(harness.inputs.find((input) => input.type === "session.open")).toMatchObject({ + config: { providerOptions: { nested: { base: true, replace: "agent" } } }, + }); + await agent.session?.close(); + }); + + test.each([ + { options: { nested: { base: true, replace: "config" }, list: [1, 2], scalar: "config" } }, + { params: { nested: { base: true, replace: "config" }, list: [1, 2], scalar: "config" } }, + { + params: { ignored: true }, + options: { nested: { base: true, replace: "config" }, list: [1, 2], scalar: "config" }, + }, + ])( + "merges configured options into plugin session.open, preserving caller input: %j", + async (override) => { + const logger = createTestLogger(); + const harness = createProviderHarness(); + const plugins = new PluginAgentClientRegistry(logger); + plugins.replace([harness.registration]); + const registry = buildProviderRegistry(logger, { + pluginProviders: plugins.definitions(), + providerOverrides: { "plugin-direct": ProviderOverrideSchema.parse(override) }, + }); + const client = registry["plugin-direct"].createClient(logger); + const defaultsSession = await client.createSession({ + provider: "plugin-direct", + cwd: "/tmp", + }); + const defaultsOpen = harness.inputs.findLast((input) => input.type === "session.open")!; + expect(defaultsOpen.config.providerOptions).toEqual({ + nested: { base: true, replace: "config" }, + list: [1, 2], + scalar: "config", + }); + await defaultsSession.close(); + const config = { + provider: "plugin-direct", + cwd: "/tmp", + providerOptions: { nested: { replace: "agent" }, list: [3], scalar: null }, + }; + const session = await client.createSession(config); + const open = harness.inputs.findLast((input) => input.type === "session.open")!; + expect(open.config.providerOptions).toEqual({ + nested: { base: true, replace: "agent" }, + list: [3], + scalar: null, + }); + expect(config.providerOptions).toEqual({ + nested: { replace: "agent" }, + list: [3], + scalar: null, + }); + const handle = session.describePersistence()!; + await session.close(); + const updatedRegistry = buildProviderRegistry(logger, { + pluginProviders: plugins.definitions(), + providerOverrides: { + "plugin-direct": { + options: { + nested: { base: false, replace: "new-config" }, + list: [4], + scalar: "new-config", + }, + }, + }, + }); + const resumed = await updatedRegistry["plugin-direct"] + .createClient(logger) + .resumeSession(handle, config); + const resumedOpen = harness.inputs.findLast((input) => input.type === "session.open")!; + expect(resumedOpen.config.providerOptions).toEqual({ + nested: { base: false, replace: "agent" }, + list: [3], + scalar: null, + }); + await resumed.close(); + }, + ); + test.each([false, true])( "contains a failed session open while send is pending: %s", async (pendingSend) => { diff --git a/packages/server/src/server/agent/plugin-provider.ts b/packages/server/src/server/agent/plugin-provider.ts index af936a0f7f..a17a8f7601 100644 --- a/packages/server/src/server/agent/plugin-provider.ts +++ b/packages/server/src/server/agent/plugin-provider.ts @@ -1,7 +1,7 @@ import { randomUUID } from "node:crypto"; import { resolve as resolvePath } from "node:path"; import type { Logger } from "pino"; -import type { JsonValue, ProviderOptions } from "@getpaseo/protocol/agent-types"; +import type { JsonValue } from "@getpaseo/protocol/agent-types"; import { z } from "zod"; import { PROVIDER_CAPABILITIES, @@ -876,8 +876,6 @@ export class PluginAgentClientRegistry { } } -const PluginProviderOptionsSchema: z.ZodType = z.record(z.string(), z.json()); - function createPluginProviderDefinition( registration: ProviderRegistration, createClient: RegisteredProviderDefinition["createClient"], @@ -889,7 +887,6 @@ function createPluginProviderDefinition( iconSvg: registration.icon, defaultModeId: null, modes: [], - optionsSchema: PluginProviderOptionsSchema, supportsExactMcpPreapproval: true, createClient, }; @@ -1625,9 +1622,7 @@ function mapSessionConfig( mode: config.modeId, thinkingOption: config.thinkingOptionId, settings: toJsonObject(config.featureValues ?? {}, "provider settings"), - providerOptions: config.providerOptions - ? toJsonObject(config.providerOptions, "provider options") - : undefined, + providerOptions: config.providerOptions, title: config.title ?? undefined, persist, }; diff --git a/packages/server/src/server/agent/provider-options.test.ts b/packages/server/src/server/agent/provider-options.test.ts index 9b642d8b5e..f5b97d064a 100644 --- a/packages/server/src/server/agent/provider-options.test.ts +++ b/packages/server/src/server/agent/provider-options.test.ts @@ -1,7 +1,11 @@ +import { mkdtempSync, writeFileSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; import { describe, expect, test } from "vitest"; import { z } from "zod"; import { createTestLogger } from "../../test-utils/test-logger.js"; +import { AgentManager } from "./agent-manager.js"; import { validateProviderOptions } from "./provider-options.js"; import { applyClaudeToolPolicy, ClaudeProviderOptionsSchema } from "./providers/claude/options.js"; import { applyCodexToolPolicy, CodexProviderOptionsSchema } from "./providers/codex/options.js"; @@ -199,3 +203,66 @@ describe("exact MCP preapproval mappings", () => { ]); }); }); + +describe("configured provider options", () => { + test.each(["options", "params"] as const)( + "Pi imports read sessionDir from config %s", + async (key) => { + const dir = mkdtempSync(join(tmpdir(), "provider-options-import-")); + try { + const file = join(dir, "native.jsonl"); + writeFileSync( + file, + JSON.stringify({ + type: "session", + version: 3, + id: "native", + cwd: "/tmp/options-project", + }) + "\n", + ); + const logger = createTestLogger(); + const registry = buildProviderRegistry(logger, { + providerOverrides: { pi: { [key]: { sessionDir: dir } } }, + }); + const sessions = await registry.pi.createClient(logger).listImportableSessions!({ + cwd: "/tmp/options-project", + }); + expect(sessions.map((session) => session.providerHandleId)).toEqual([file]); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }, + ); + + test("a provider without option handling ignores the record", async () => { + const logger = createTestLogger(); + const registry = buildProviderRegistry(logger, { + isDev: true, + providerOverrides: { mock: { options: { arbitrary: { base: true } } } }, + }); + const session = await registry.mock + .createClient(logger) + .createSession({ provider: "mock", cwd: "/tmp", providerOptions: { more: [1] } }); + expect(session.provider).toBe("mock"); + await session.close(); + }); + + test("config options reach Claude and provider validation reaches the create caller", async () => { + const logger = createTestLogger(); + const registry = buildProviderRegistry(logger, { + providerOverrides: { + claude: { options: { sandbox: { network: { allowLocalBinding: "yes" } } } }, + }, + }); + const manager = new AgentManager({ + logger, + clients: { claude: registry.claude.createClient(logger) }, + providerDefinitions: { claude: registry.claude }, + }); + await expect( + manager.createAgent({ provider: "claude", cwd: "/tmp", model: "sonnet" }, undefined, { + workspaceId: undefined, + }), + ).rejects.toThrow("providerOptions.sandbox.network.allowLocalBinding"); + }); +}); diff --git a/packages/server/src/server/agent/provider-options.ts b/packages/server/src/server/agent/provider-options.ts index bf02410e22..8a7d0b16af 100644 --- a/packages/server/src/server/agent/provider-options.ts +++ b/packages/server/src/server/agent/provider-options.ts @@ -36,11 +36,11 @@ export class ToolPolicyUnsupportedError extends Error { } } -export function validateProviderOptions( +export function validateProviderOptions( provider: string, - schema: z.ZodType, + schema: z.ZodType, options: ProviderOptions | undefined, -): ProviderOptions | undefined { +): T | undefined { if (options === undefined) return undefined; const parsed = schema.safeParse(options); if (parsed.success) return parsed.data; @@ -75,3 +75,31 @@ function formatProviderOptionPath(path: Array): string { : `${formatted}[${JSON.stringify(segment)}]`; }, "providerOptions"); } + +/** Plain objects merge recursively; all other values replace the base value. */ +export function mergeProviderOptions( + base: ProviderOptions | undefined, + override: ProviderOptions | undefined, +): ProviderOptions | undefined { + if (base === undefined && override === undefined) return undefined; + const merged: ProviderOptions = { ...base }; + for (const [key, value] of Object.entries(override ?? {})) { + const previous = merged[key]; + Object.defineProperty(merged, key, { + value: + isPlainObject(previous) && isPlainObject(value) + ? mergeProviderOptions(previous, value) + : value, + enumerable: true, + configurable: true, + writable: true, + }); + } + return merged; +} + +function isPlainObject(value: unknown): value is ProviderOptions { + if (value === null || typeof value !== "object") return false; + const prototype = Object.getPrototypeOf(value); + return prototype === Object.prototype || prototype === null; +} diff --git a/packages/server/src/server/agent/provider-registry-wrap.test.ts b/packages/server/src/server/agent/provider-registry-wrap.test.ts index ae1254efa7..04d123ead1 100644 --- a/packages/server/src/server/agent/provider-registry-wrap.test.ts +++ b/packages/server/src/server/agent/provider-registry-wrap.test.ts @@ -6,6 +6,9 @@ import type { AgentSession, AgentStreamEvent, AgentRuntimeInfo, + SteerActiveTurnOptions, + SteerResult, + ImportedTimelineEntry, } from "./agent-sdk-types.js"; import { wrapSessionProvider } from "./provider-registry.js"; @@ -18,6 +21,7 @@ type OptionalAgentSessionMethodName = { }[keyof AgentSession]; const OPTIONAL_AGENT_SESSION_METHOD_NAMES = [ + "steerActiveTurn", "listCommands", "setModel", "setThinkingOption", @@ -56,8 +60,12 @@ const RUNTIME_INFO: AgentRuntimeInfo = { class FakeSession implements AgentSession { readonly provider = "claude"; - readonly id = "session-1"; - readonly capabilities = CAPABILITIES; + id = "session-1"; + capabilities = CAPABILITIES; + initialTimeline: ImportedTimelineEntry[] = [ + { item: { type: "assistant_message", id: "initial", text: "Provider setup" } }, + ]; + readonly steers: Array<{ prompt: AgentPromptInput; options: SteerActiveTurnOptions }> = []; readonly features = []; readonly recordedCalls: string[] = []; @@ -71,6 +79,15 @@ class FakeSession implements AgentSession { return { turnId: "turn-1" }; } + async steerActiveTurn( + prompt: AgentPromptInput, + options: SteerActiveTurnOptions, + ): Promise { + this.recordedCalls.push("steerActiveTurn"); + this.steers.push({ prompt, options }); + return { status: "accepted" }; + } + subscribe(_callback: (event: AgentStreamEvent) => void) { this.recordedCalls.push("subscribe"); return () => {}; @@ -172,6 +189,7 @@ describe("wrapSessionProvider", () => { const session = new FakeSession(); const wrapped = wrapSessionProvider("custom-claude", session); + await wrapped.steerActiveTurn?.("follow-up", { expectedTurnId: "turn-1" }); await wrapped.listCommands?.(); await wrapped.setModel?.("sonnet"); await wrapped.setThinkingOption?.("high"); @@ -182,7 +200,11 @@ describe("wrapSessionProvider", () => { const handler = wrapped.tryHandleOutOfBand?.("/compact"); await handler?.run({ emit: () => {} }); + expect(session.steers).toEqual([ + { prompt: "follow-up", options: { expectedTurnId: "turn-1" } }, + ]); expect(session.recordedCalls).toEqual([ + "steerActiveTurn", "listCommands", "setModel", "setThinkingOption", @@ -194,4 +216,34 @@ describe("wrapSessionProvider", () => { "tryHandleOutOfBand.run", ]); }); + test("keeps provider-owned session values live", () => { + const session = new FakeSession(); + const wrapped = wrapSessionProvider("custom-claude", session); + session.id = "session-2"; + session.capabilities = { ...CAPABILITIES, supportsMcpServers: false }; + expect(wrapped.id).toBe("session-2"); + expect(wrapped.capabilities).toEqual(session.capabilities); + expect(wrapped.initialTimeline).toEqual(session.initialTimeline); + }); + + test("propagates steering failure without interrupting or replacing the turn", async () => { + const error = new Error("Provider steer transport failed"); + class RejectingSession extends FakeSession { + override async steerActiveTurn(): Promise { + throw error; + } + } + const session = new RejectingSession(); + const wrapped = wrapSessionProvider("custom-claude", session); + await expect(wrapped.steerActiveTurn!("follow-up", { expectedTurnId: "turn-1" })).rejects.toBe( + error, + ); + expect(session.recordedCalls).toEqual([]); + }); + + test("leaves steering unavailable when the provider has no implementation", () => { + const session: AgentSession = new FakeSession(); + session.steerActiveTurn = undefined; + expect(wrapSessionProvider("custom-claude", session).steerActiveTurn).toBeUndefined(); + }); }); diff --git a/packages/server/src/server/agent/provider-registry.test.ts b/packages/server/src/server/agent/provider-registry.test.ts index 9c4a22a4f8..9a2cb768f0 100644 --- a/packages/server/src/server/agent/provider-registry.test.ts +++ b/packages/server/src/server/agent/provider-registry.test.ts @@ -24,7 +24,7 @@ const CLAUDE_CUSTOM_THINKING_FIELDS = { const mockState = vi.hoisted(() => { interface ConstructorEntry { runtimeSettings?: unknown; - providerParams?: unknown; + commandsRpcType?: unknown; } @@ -36,17 +36,14 @@ const mockState = vi.hoisted(() => { cursor: [] as Array<{ command: string[]; env?: Record; - providerParams?: unknown; }>, trae: [] as Array<{ command: string[]; env?: Record; - providerParams?: unknown; }>, kimi: [] as Array<{ command: string[]; env?: Record; - providerParams?: unknown; }>, pi: [] as ConstructorEntry[], genericAcp: [] as Array<{ @@ -54,7 +51,6 @@ const mockState = vi.hoisted(() => { env?: Record; providerId?: string; label?: string; - providerParams?: unknown; }>, }, isCommandAvailable: vi.fn(async (_command: string) => false), @@ -261,13 +257,12 @@ vi.mock("./providers/pi/agent.js", () => ({ constructor(options: { runtimeSettings?: unknown; - providerParams?: unknown; + commandsRpcType?: unknown; }) { this.runtimeSettings = options.runtimeSettings; const entry: ConstructorEntry = { runtimeSettings: options.runtimeSettings, - providerParams: options.providerParams, }; if (options.commandsRpcType !== undefined) { entry.commandsRpcType = options.commandsRpcType; @@ -314,21 +309,7 @@ vi.mock("./providers/generic-acp-agent.js", () => ({ env?: Record; providerId?: string; label?: string; - providerParams?: unknown; }) { - const providerParams = - options.providerParams && - typeof options.providerParams === "object" && - !Array.isArray(options.providerParams) - ? (options.providerParams as Record) - : {}; - this.capabilities = { - ...this.capabilities, - supportsMcpServers: - typeof providerParams.supportsMcpServers === "boolean" - ? providerParams.supportsMcpServers - : this.capabilities.supportsMcpServers, - }; this.runtimeSettings = { command: { mode: "replace", @@ -341,7 +322,6 @@ vi.mock("./providers/generic-acp-agent.js", () => ({ env: options.env, providerId: options.providerId, label: options.label, - providerParams: options.providerParams, }); } @@ -379,11 +359,7 @@ vi.mock("./providers/cursor-acp-agent.js", () => ({ readonly provider = "acp"; readonly runtimeSettings?: unknown; - constructor(options: { - command: string[]; - env?: Record; - providerParams?: unknown; - }) { + constructor(options: { command: string[]; env?: Record }) { this.runtimeSettings = { command: { mode: "replace", @@ -394,7 +370,6 @@ vi.mock("./providers/cursor-acp-agent.js", () => ({ mockState.constructorArgs.cursor.push({ command: options.command, env: options.env, - providerParams: options.providerParams, }); } @@ -445,11 +420,7 @@ vi.mock("./providers/trae-acp-agent.js", () => ({ readonly provider = "acp"; readonly runtimeSettings?: unknown; - constructor(options: { - command: string[]; - env?: Record; - providerParams?: unknown; - }) { + constructor(options: { command: string[]; env?: Record }) { this.runtimeSettings = { command: { mode: "replace", @@ -460,7 +431,6 @@ vi.mock("./providers/trae-acp-agent.js", () => ({ mockState.constructorArgs.trae.push({ command: options.command, env: options.env, - providerParams: options.providerParams, }); } @@ -498,11 +468,7 @@ vi.mock("./providers/kimi-acp-agent.js", () => ({ readonly provider = "acp"; readonly runtimeSettings?: unknown; - constructor(options: { - command: string[]; - env?: Record; - providerParams?: unknown; - }) { + constructor(options: { command: string[]; env?: Record }) { this.runtimeSettings = { command: { mode: "replace", @@ -513,7 +479,6 @@ vi.mock("./providers/kimi-acp-agent.js", () => ({ mockState.constructorArgs.kimi.push({ command: options.command, env: options.env, - providerParams: options.providerParams, }); } @@ -728,7 +693,6 @@ test("new provider extending acp uses GenericACPAgentClient", () => { }, providerId: "my-agent", label: "My Agent", - providerParams: undefined, }, { command: ["my-agent", "--acp"], @@ -737,7 +701,6 @@ test("new provider extending acp uses GenericACPAgentClient", () => { }, providerId: "my-agent", label: "My Agent", - providerParams: undefined, }, ]); }); @@ -812,43 +775,20 @@ test("ordinary custom ACP providers remain fail-closed for exact MCP grants", () ).toThrow(/cannot preapprove exact MCP tools for unattended execution/u); }); -test("ACP provider params can disable MCP support", () => { +test("ACP provider configuration retains opaque options", () => { const registry = buildProviderRegistry(logger, { providerOverrides: { "no-mcp-acp": { extends: "acp", label: "No MCP ACP", command: ["no-mcp-acp", "serve"], - params: { - supportsMcpServers: false, - }, + options: { supportsMcpServers: false }, }, }, }); - - const client = registry["no-mcp-acp"].createClient(logger); - - expect(client.capabilities.supportsMcpServers).toBe(false); - expect(mockState.constructorArgs.genericAcp).toEqual([ - { - command: ["no-mcp-acp", "serve"], - env: undefined, - providerId: "no-mcp-acp", - label: "No MCP ACP", - providerParams: { - supportsMcpServers: false, - }, - }, - { - command: ["no-mcp-acp", "serve"], - env: undefined, - providerId: "no-mcp-acp", - label: "No MCP ACP", - providerParams: { - supportsMcpServers: false, - }, - }, - ]); + expect(registry["no-mcp-acp"].configuration?.providerOptions).toEqual({ + supportsMcpServers: false, + }); }); test("cursor provider extending acp uses CursorACPAgentClient", () => { @@ -872,14 +812,12 @@ test("cursor provider extending acp uses CursorACPAgentClient", () => { env: { CURSOR_AGENT_LOG: "debug", }, - providerParams: undefined, }, { command: ["cursor-agent", "acp"], env: { CURSOR_AGENT_LOG: "debug", }, - providerParams: undefined, }, ]); expect(mockState.constructorArgs.genericAcp).toEqual([]); @@ -936,12 +874,10 @@ test("traecli provider extending acp uses TraeACPAgentClient", () => { { command: ["traecli", "acp", "serve"], env: undefined, - providerParams: undefined, }, { command: ["traecli", "acp", "serve"], env: undefined, - providerParams: undefined, }, ]); expect(mockState.constructorArgs.genericAcp).toEqual([]); @@ -963,12 +899,10 @@ test("kimi provider extending acp uses KimiACPAgentClient", () => { { command: ["kimi", "acp"], env: undefined, - providerParams: undefined, }, { command: ["kimi", "acp"], env: undefined, - providerParams: undefined, }, ]); expect(mockState.constructorArgs.genericAcp).toEqual([]); diff --git a/packages/server/src/server/agent/provider-registry.ts b/packages/server/src/server/agent/provider-registry.ts index 004f16c6c4..3a66ffcfcf 100644 --- a/packages/server/src/server/agent/provider-registry.ts +++ b/packages/server/src/server/agent/provider-registry.ts @@ -1,6 +1,5 @@ import type { Logger } from "pino"; import type { ProviderOptions, ToolPolicy } from "@getpaseo/protocol/agent-types"; -import { z } from "zod"; import type { AgentClient, @@ -49,10 +48,7 @@ import { PiRpcAgentClient } from "./providers/pi/agent.js"; import { TraeACPAgentClient } from "./providers/trae-acp-agent.js"; import { MockLoadTestAgentClient } from "./providers/mock-load-test-agent.js"; import { MockSlowProviderClient } from "./providers/mock-slow-provider.js"; -import { ClaudeProviderOptionsSchema } from "./providers/claude/options.js"; -import { CodexProviderOptionsSchema } from "./providers/codex/options.js"; -import { OpenCodeProviderOptionsSchema } from "./providers/opencode/options.js"; -import { ToolPolicyUnsupportedError, validateProviderOptions } from "./provider-options.js"; +import { ToolPolicyUnsupportedError, mergeProviderOptions } from "./provider-options.js"; import { AGENT_PROVIDER_DEFINITIONS, BUILTIN_PROVIDER_IDS, @@ -80,13 +76,7 @@ export interface ProviderDefinition extends AgentProviderDefinition { * generic ACP providers (which only extend the literal "acp" sentinel). */ derivedFromProviderId: string | null; - optionsSchema: z.ZodType; supportsExactMcpPreapproval: boolean; - validateOptions: (options: ProviderOptions | undefined) => ProviderOptions | undefined; - applyOptions: ( - config: AgentSessionConfig, - options: ProviderOptions | undefined, - ) => AgentSessionConfig; applyToolPolicy: ( config: AgentSessionConfig, toolPolicy: ToolPolicy | undefined, @@ -108,7 +98,6 @@ export interface ProviderDefinition extends AgentProviderDefinition { /** Provider metadata and an unconfigured factory; the registry owns override application. */ export interface RegisteredProviderDefinition extends AgentProviderDefinition { iconSvg?: string; - optionsSchema: z.ZodType; supportsExactMcpPreapproval: boolean; createClient: (logger: Logger, runtimeSettings?: ProviderRuntimeSettings) => AgentClient; } @@ -129,7 +118,6 @@ interface ProviderClientFactoryOptions extends Pick< "workspaceGitService" | "managedProcesses" | "ompRuntime" > { openCodeBridge?: OpenCodeBridge; - providerParams?: unknown; customProvider?: { id: string; label: string; @@ -151,27 +139,23 @@ interface ResolvedProvider { profileModelsAreAdditive: boolean; enabled: boolean; derivedFromProviderId: string | null; - providerParams?: unknown; + providerOptions?: ProviderOptions; createBaseClient: (logger: Logger) => AgentClient; contract: ProviderContract; } interface ProviderContract { - optionsSchema: z.ZodType; supportsExactMcpPreapproval: boolean; applyToolPolicy?: (provider: string, toolPolicy: ToolPolicy) => ToolPolicy; } -const EmptyProviderOptionsSchema: z.ZodType = z.object({}).strict(); - const PROVIDER_CONTRACTS: Record = { - claude: { optionsSchema: ClaudeProviderOptionsSchema, supportsExactMcpPreapproval: true }, - codex: { optionsSchema: CodexProviderOptionsSchema, supportsExactMcpPreapproval: true }, - opencode: { optionsSchema: OpenCodeProviderOptionsSchema, supportsExactMcpPreapproval: true }, + claude: { supportsExactMcpPreapproval: true }, + codex: { supportsExactMcpPreapproval: true }, + opencode: { supportsExactMcpPreapproval: true }, }; const UNSUPPORTED_PROVIDER_CONTRACT: ProviderContract = { - optionsSchema: EmptyProviderOptionsSchema, supportsExactMcpPreapproval: false, }; @@ -181,7 +165,6 @@ const HUB_E2E_TOOL_NAME = /^[A-Za-z0-9][A-Za-z0-9_.-]*$/u; // The cross-repository Hub harness owns this synthetic provider ID. It exercises the production // registry path without extending exact-preapproval support to user-defined ACP providers. const HUB_E2E_PROVIDER_CONTRACT: ProviderContract = { - optionsSchema: EmptyProviderOptionsSchema, supportsExactMcpPreapproval: true, applyToolPolicy: (provider, toolPolicy) => { for (const grant of toolPolicy.preapproved) { @@ -229,17 +212,15 @@ const PROVIDER_CLIENT_FACTORIES: Record = { managedProcesses: options?.managedProcesses, bridge: options?.openCodeBridge, }), - pi: (logger, runtimeSettings, options) => + pi: (logger, runtimeSettings) => new PiRpcAgentClient({ logger, runtimeSettings, - providerParams: options?.providerParams, }), omp: (logger, runtimeSettings, options) => new OmpAgentClient({ logger, runtimeSettings, - providerParams: options?.providerParams, runtime: options?.ompRuntime, }), mock: (logger) => new MockLoadTestAgentClient(logger), @@ -450,16 +431,27 @@ function mergeModelAdditions( ); } +// Every session member must cross this boundary, including optional capabilities. +type ForwardedAgentSession = { [K in keyof Required]: AgentSession[K] }; + export function wrapSessionProvider(provider: AgentProvider, inner: AgentSession): AgentSession { return { provider, - id: inner.id, - capabilities: inner.capabilities, + get id() { + return inner.id; + }, + get capabilities() { + return inner.capabilities; + }, + get initialTimeline() { + return inner.initialTimeline; + }, get features() { return inner.features; }, run: (prompt, options) => inner.run(prompt, options), startTurn: (prompt, options) => inner.startTurn(prompt, options), + steerActiveTurn: inner.steerActiveTurn?.bind(inner), subscribe: (callback) => inner.subscribe((event) => callback(mapStreamEvent(provider, event))), async *streamHistory() { for await (const event of inner.streamHistory()) { @@ -483,7 +475,7 @@ export function wrapSessionProvider(provider: AgentProvider, inner: AgentSession revertFiles: inner.revertFiles?.bind(inner), revertBoth: inner.revertBoth?.bind(inner), tryHandleOutOfBand: inner.tryHandleOutOfBand?.bind(inner), - }; + } satisfies ForwardedAgentSession; } function wrapClientProvider( @@ -492,6 +484,7 @@ function wrapClientProvider( profileModels: ProviderProfileModel[], additionalModels: ProviderProfileModel[], profileModelsAreAdditive: boolean, + providerOptions: ProviderOptions | undefined, ): AgentClient { const listImportableSessions = inner.listImportableSessions?.bind(inner); const importSession = inner.importSession?.bind(inner); @@ -512,6 +505,7 @@ function wrapClientProvider( { ...config, provider: inner.provider, + providerOptions: mergeProviderOptions(providerOptions, config.providerOptions), }, launchContext, options, @@ -525,18 +519,17 @@ function wrapClientProvider( ...handle, provider: inner.provider, }, - overrides - ? { - ...overrides, - provider: inner.provider, - } - : undefined, + { + ...overrides, + provider: inner.provider, + providerOptions: mergeProviderOptions(providerOptions, overrides?.providerOptions), + }, launchContext, options, ), ), fetchCatalog: async (options, context) => { - const catalog = await inner.fetchCatalog(options, context); + const catalog = await inner.fetchCatalog({ ...options, providerOptions }, context); return { ...catalog, models: mergeModels(provider, profileModels, additionalModels, catalog.models, { @@ -548,7 +541,11 @@ function wrapClientProvider( resolveDefaultModeId: inner.resolveDefaultModeId ? async ({ config, env, signal }: ResolveAgentDefaultModeInput) => await inner.resolveDefaultModeId?.({ - config: { ...config, provider: inner.provider }, + config: { + ...config, + provider: inner.provider, + providerOptions: mergeProviderOptions(providerOptions, config.providerOptions), + }, env, signal, }) @@ -557,10 +554,15 @@ function wrapClientProvider( resolveConfiguredModel: inner.resolveConfiguredModel?.bind(inner), isCreateConfigUnattended: inner.isCreateConfigUnattended?.bind(inner), listFeatures: listFeatures - ? async (config) => await listFeatures({ ...config, provider: inner.provider }) + ? async (config) => + await listFeatures({ + ...config, + provider: inner.provider, + providerOptions: mergeProviderOptions(providerOptions, config.providerOptions), + }) : undefined, listImportableSessions: listImportableSessions - ? async (options) => await listImportableSessions(options) + ? async (options) => await listImportableSessions({ ...options, providerOptions }) : undefined, importSession: importSession ? async (input, context) => { @@ -569,6 +571,10 @@ function wrapClientProvider( config: { ...context.config, provider: inner.provider, + providerOptions: mergeProviderOptions( + providerOptions, + context.config.providerOptions, + ), }, storedConfig: { ...context.storedConfig, @@ -585,6 +591,7 @@ function wrapClientProvider( config: { ...imported.config, provider, + providerOptions: context.storedConfig.providerOptions, }, persistence, }; @@ -607,7 +614,7 @@ function createRegistryEntry( provider: AgentProvider, resolved: ResolvedProvider, ): ProviderDefinition { - const modelClient = resolved.createBaseClient(logger); + const modelClient = createResolvedProviderClient(logger, provider, resolved); const profileModels = resolveConfiguredModels(provider, modelClient, resolved.profileModels); const additionalModels = resolveConfiguredModels( provider, @@ -638,11 +645,7 @@ function createRegistryEntry( configuration, enabled: resolved.enabled, derivedFromProviderId: resolved.derivedFromProviderId, - optionsSchema: resolved.contract.optionsSchema, supportsExactMcpPreapproval: resolved.contract.supportsExactMcpPreapproval, - validateOptions: (options) => - validateProviderOptions(provider, resolved.contract.optionsSchema, options), - applyOptions: (config, options) => ({ ...config, providerOptions: options }), applyToolPolicy: (config, toolPolicy) => { if (toolPolicy && !resolved.contract.supportsExactMcpPreapproval) { throw new ToolPolicyUnsupportedError(provider); @@ -713,16 +716,13 @@ function createResolvedProviderClient( const inner = resolved.createBaseClient(logger); const profileModels = resolveConfiguredModels(provider, inner, resolved.profileModels); const additionalModels = resolveConfiguredModels(provider, inner, resolved.additionalModels); - const hasModelOverrides = profileModels.length > 0 || additionalModels.length > 0; - if (inner.provider === provider && !hasModelOverrides) { - return inner; - } return wrapClientProvider( provider, inner, profileModels, additionalModels, resolved.profileModelsAreAdditive, + resolved.providerOptions, ); } @@ -745,7 +745,7 @@ function resolveRegisteredProvider(input: RegisteredProvider): ResolvedProvider profileModelsAreAdditive: false, enabled: override?.enabled ?? definition.enabledByDefault ?? true, derivedFromProviderId: null, - providerParams: override?.params, + providerOptions: configuredProviderOptions(override), createBaseClient: (logger) => input.createClient(logger, runtimeSettings), contract, }; @@ -781,7 +781,6 @@ function buildResolvedBuiltinProviders( managedProcesses: options.managedProcesses, ompRuntime: options.ompRuntime, openCodeBridge: options.openCodeBridge, - providerParams: override?.params, }), contract: PROVIDER_CONTRACTS[definition.id] ?? UNSUPPORTED_PROVIDER_CONTRACT, }), @@ -831,7 +830,7 @@ function addDerivedProviders( profileModelsAreAdditive: false, enabled: override.enabled !== false, derivedFromProviderId: null, - providerParams: override.params, + providerOptions: configuredProviderOptions(override), createBaseClient: (logger) => { const acpOptions = { logger, @@ -839,7 +838,6 @@ function addDerivedProviders( env: override.env, providerId, label: override.label ?? providerId, - providerParams: override.params, }; if (providerId === "cursor") { return new CursorACPAgentClient(acpOptions); @@ -877,7 +875,7 @@ function addDerivedProviders( ); const baseDefinition = baseProvider.definition; const baseFactory = getProviderClientFactory(baseProviderId); - const providerParams = override.params ?? baseProvider.providerParams; + const providerOptions = configuredProviderOptions(override) ?? baseProvider.providerOptions; resolvedProviders.set(providerId, { definition: createDerivedDefinition(providerId, baseDefinition, override), @@ -887,12 +885,11 @@ function addDerivedProviders( profileModelsAreAdditive: false, enabled: override.enabled !== false, derivedFromProviderId: baseProviderId, - providerParams, + providerOptions, createBaseClient: (logger) => baseFactory(logger, mergedRuntimeSettings, { managedProcesses: options.managedProcesses, openCodeBridge: options.openCodeBridge, - providerParams, customProvider: { id: providerId, label: override.label ?? providerId, @@ -938,7 +935,6 @@ export function buildProviderRegistry( runtimeSettings: runtimeSettings?.[provider], createClient: definition.createClient, contract: { - optionsSchema: definition.optionsSchema, supportsExactMcpPreapproval: definition.supportsExactMcpPreapproval, }, }), @@ -1009,3 +1005,11 @@ export async function shutdownAgentClients( }), ); } + +function configuredProviderOptions( + override: ProviderOverride | undefined, +): ProviderOptions | undefined { + // COMPAT(providerParams): added in v0.10.0, remove after 2027-03-30 once configs use options. + // options wins as a whole record when both spellings are supplied. + return override?.options ?? override?.params; +} diff --git a/packages/server/src/server/agent/provider-snapshot-manager.test.ts b/packages/server/src/server/agent/provider-snapshot-manager.test.ts index b8c59d739d..66d86f8931 100644 --- a/packages/server/src/server/agent/provider-snapshot-manager.test.ts +++ b/packages/server/src/server/agent/provider-snapshot-manager.test.ts @@ -238,7 +238,7 @@ describe("ProviderSnapshotManager public surface", () => { } }); - test("validates complete Hub agent configurations through the current provider contract", async () => { + test("validates catalog choices while leaving provider options opaque", async () => { const manager = new ProviderSnapshotManager({ logger: createTestLogger(), extraClients: { @@ -293,11 +293,19 @@ describe("ProviderSnapshotManager public surface", () => { path: ["thinkingOptionId"], message: "Thinking option 'missing' is not available for provider 'codex'", }, - { - path: ["providerOptions", "sandbox_workspace_write", "network_access"], - message: "Invalid input: expected boolean, received string", - }, ]); + await expect( + manager.validateAgentConfiguration({ + provider: "codex", + model: "gpt-latest", + modeId: "auto-review", + thinkingOptionId: "xhigh", + providerOptions: { + sandbox_workspace_write: { network_access: "sometimes" }, + arbitrary: true, + }, + }), + ).resolves.toEqual([]); } finally { manager.destroy(); } diff --git a/packages/server/src/server/agent/provider-snapshot-manager.ts b/packages/server/src/server/agent/provider-snapshot-manager.ts index 22a706a279..82acd13e1f 100644 --- a/packages/server/src/server/agent/provider-snapshot-manager.ts +++ b/packages/server/src/server/agent/provider-snapshot-manager.ts @@ -185,10 +185,7 @@ export interface AgentManagerProviderState { providerDefinitions: Partial< Record< AgentProvider, - Pick< - ProviderDefinition, - "enabled" | "derivedFromProviderId" | "validateOptions" | "applyOptions" | "applyToolPolicy" - > + Pick > >; clients: Partial>; @@ -369,8 +366,6 @@ export class ProviderSnapshotManager { providerDefinitions[provider] = { enabled: definition.enabled, derivedFromProviderId: definition.derivedFromProviderId, - validateOptions: definition.validateOptions, - applyOptions: definition.applyOptions, applyToolPolicy: definition.applyToolPolicy, }; if (definition.enabled) { @@ -516,11 +511,9 @@ export class ProviderSnapshotManager { ]; } - const definition = this.requireProvider(input.provider); return validateAgentConfigurationAgainstProvider({ input, provider, - validateOptions: definition.validateOptions, }); } diff --git a/packages/server/src/server/agent/providers/acp-agent.ts b/packages/server/src/server/agent/providers/acp-agent.ts index b097f8b1e5..524ede92dd 100644 --- a/packages/server/src/server/agent/providers/acp-agent.ts +++ b/packages/server/src/server/agent/providers/acp-agent.ts @@ -1,3 +1,4 @@ +import { ACPProviderOptionsSchema } from "./acp-options.js"; import { type ChildProcess, type ChildProcessWithoutNullStreams } from "node:child_process"; import { randomUUID } from "node:crypto"; import fs from "node:fs/promises"; @@ -961,6 +962,7 @@ export class ACPAgentClient implements AgentClient { launchContext?: AgentLaunchContext, ): Promise { this.assertProvider(config); + const providerOptions = ACPProviderOptionsSchema.parse(config.providerOptions ?? {}); const session = new ACPAgentSession( { ...config, provider: this.provider }, { @@ -973,14 +975,18 @@ export class ACPAgentClient implements AgentClient { sessionResponseTransformer: this.sessionResponseTransformer, configOptionsTransformer: this.configOptionsTransformer, configFeatureOptions: this.configFeatureOptions, - clientCapabilities: this.clientCapabilities, + clientCapabilities: providerOptions.clientCapabilities ?? this.clientCapabilities, clientCapabilityMeta: this.clientCapabilityMeta, modeIdTransformer: this.modeIdTransformer, toolSnapshotTransformer: this.toolSnapshotTransformer, providerModeWriter: this.providerModeWriter, beforeModeWriter: this.beforeModeWriter, thinkingOptionWriter: this.thinkingOptionWriter, - capabilities: this.capabilities, + capabilities: { + ...this.capabilities, + supportsMcpServers: + providerOptions.supportsMcpServers ?? this.capabilities.supportsMcpServers, + }, agentId: launchContext?.agentId, launchEnv: launchContext?.env, extensionCommandsParser: this.extensionCommandsParser, @@ -1013,6 +1019,7 @@ export class ACPAgentClient implements AgentClient { provider: this.provider, cwd, }; + const providerOptions = ACPProviderOptionsSchema.parse(mergedConfig.providerOptions ?? {}); const session = new ACPAgentSession(mergedConfig, { provider: this.provider, logger: this.logger, @@ -1023,14 +1030,18 @@ export class ACPAgentClient implements AgentClient { sessionResponseTransformer: this.sessionResponseTransformer, configOptionsTransformer: this.configOptionsTransformer, configFeatureOptions: this.configFeatureOptions, - clientCapabilities: this.clientCapabilities, + clientCapabilities: providerOptions.clientCapabilities ?? this.clientCapabilities, clientCapabilityMeta: this.clientCapabilityMeta, modeIdTransformer: this.modeIdTransformer, toolSnapshotTransformer: this.toolSnapshotTransformer, providerModeWriter: this.providerModeWriter, beforeModeWriter: this.beforeModeWriter, thinkingOptionWriter: this.thinkingOptionWriter, - capabilities: this.capabilities, + capabilities: { + ...this.capabilities, + supportsMcpServers: + providerOptions.supportsMcpServers ?? this.capabilities.supportsMcpServers, + }, handle, agentId: launchContext?.agentId, launchEnv: launchContext?.env, @@ -1082,6 +1093,7 @@ export class ACPAgentClient implements AgentClient { raceProviderRefreshAbort( context?.signal, this.spawnProcess(PROBE_ENV, { + providerOptions: options.providerOptions, onSpawned: (spawned) => { probe = spawned; if (context?.signal.aborted) void closeProbe().catch(() => undefined); @@ -1327,6 +1339,7 @@ export class ACPAgentClient implements AgentClient { launchEnv?: Record, options?: { initializeTimeoutMs?: number; + providerOptions?: Record; onSpawned?: (probe: UninitializedACPProcess) => void; client?: ACPClient; }, @@ -1339,7 +1352,11 @@ export class ACPAgentClient implements AgentClient { }; options?.onSpawned?.(probe); try { - const initialize = await this.initializeTransport(transport, options?.initializeTimeoutMs); + const initialize = await this.initializeTransport( + transport, + options?.initializeTimeoutMs, + options?.providerOptions, + ); const initializedProbe: SpawnedACPProcess = { ...probe, initialize, @@ -1398,7 +1415,9 @@ export class ACPAgentClient implements AgentClient { protected async initializeTransport( transport: ACPProcessTransport, initializeTimeoutMs?: number, + providerOptions?: Record, ): Promise { + const configuredOptions = ACPProviderOptionsSchema.parse(providerOptions ?? {}); let timeout: ReturnType | null = null; const initializeTimeoutPromise = initializeTimeoutMs ? new Promise((_, reject) => { @@ -1415,7 +1434,7 @@ export class ACPAgentClient implements AgentClient { protocolVersion: PROTOCOL_VERSION, clientCapabilities: buildACPClientCapabilities( this.clientCapabilityMeta, - this.clientCapabilities, + configuredOptions.clientCapabilities ?? this.clientCapabilities, ), clientInfo: { name: "Paseo", version: "dev" }, }), @@ -2419,12 +2438,13 @@ export class ACPAgentSession implements AgentSession, ACPClient { if (!this.sessionId) { return null; } + const { providerOptions: _providerOptions, ...persistedConfig } = this.config; return { provider: this.provider, sessionId: this.sessionId, nativeHandle: this.sessionId, metadata: { - ...this.config, + ...persistedConfig, title: this.currentTitle, }, }; diff --git a/packages/server/src/server/agent/providers/acp-options.ts b/packages/server/src/server/agent/providers/acp-options.ts new file mode 100644 index 0000000000..4f75456e38 --- /dev/null +++ b/packages/server/src/server/agent/providers/acp-options.ts @@ -0,0 +1,18 @@ +import { z } from "zod"; + +export const ACPProviderOptionsSchema = z + .object({ + supportsMcpServers: z.boolean().optional(), + clientCapabilities: z + .object({ + fs: z + .object({ + readTextFile: z.boolean().optional(), + writeTextFile: z.boolean().optional(), + }) + .optional(), + terminal: z.boolean().optional(), + }) + .optional(), + }) + .passthrough(); diff --git a/packages/server/src/server/agent/providers/claude/agent.ts b/packages/server/src/server/agent/providers/claude/agent.ts index dd79b57a25..b6a366370b 100644 --- a/packages/server/src/server/agent/providers/claude/agent.ts +++ b/packages/server/src/server/agent/providers/claude/agent.ts @@ -1,3 +1,4 @@ +import { validateProviderOptions } from "../../provider-options.js"; import type { ChildProcess } from "node:child_process"; import { randomUUID } from "node:crypto"; import fs from "node:fs"; @@ -1692,7 +1693,8 @@ export class ClaudeAgentClient implements AgentClient { throw new Error(`ClaudeAgentClient received config for provider '${config.provider}'`); } const model = config.model?.trim(); - const providerOptions = ClaudeProviderOptionsSchema.parse(config.providerOptions ?? {}); + const providerOptions = + validateProviderOptions("claude", ClaudeProviderOptionsSchema, config.providerOptions) ?? {}; return { ...config, provider: "claude", @@ -2659,11 +2661,12 @@ class ClaudeAgentSession implements AgentSession { if (!this.claudeSessionId) { return null; } + const { providerOptions: _providerOptions, ...persistedConfig } = this.config; this.persistence = { provider: "claude", sessionId: this.claudeSessionId, nativeHandle: this.claudeSessionId, - metadata: { ...this.config }, + metadata: { ...persistedConfig }, }; return this.persistence; } diff --git a/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts b/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts index ea52d982e8..bf3ab15c62 100644 --- a/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts +++ b/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts @@ -1164,6 +1164,23 @@ describe("Codex app-server provider", () => { } }); + test("provider persistence leaves options owned by the stored agent config", async () => { + const appServer = createFakeCodexAppServer(); + const provider = createProviderWithFakeAppServer(appServer); + const session = await provider.createSession( + createConfig({ providerOptions: { sandbox_mode: "read-only", approval_policy: "never" } }), + ); + try { + await session.startTurn("persist the configured session"); + await appServer.waitForTurnStart(); + const handle = session.describePersistence()!; + expect(handle.metadata).not.toHaveProperty("providerOptions"); + appServer.assertNoErrors(); + } finally { + await session.close(); + } + }); + test("preapproves only granted tools on the injected Codex MCP server", async () => { const session = createSession({ modeId: undefined, diff --git a/packages/server/src/server/agent/providers/codex-app-server-agent.ts b/packages/server/src/server/agent/providers/codex-app-server-agent.ts index b1dad5332c..81ea190454 100644 --- a/packages/server/src/server/agent/providers/codex-app-server-agent.ts +++ b/packages/server/src/server/agent/providers/codex-app-server-agent.ts @@ -1,3 +1,4 @@ +import { validateProviderOptions } from "../provider-options.js"; import { getAgentStreamEventTurnId, type AgentPermissionAction, @@ -3532,7 +3533,8 @@ export class CodexAppServerAgentSession implements AgentSession { } this.hasWorkflowModeOverride = config.modeId !== undefined; this.currentMode = config.modeId ?? DEFAULT_CODEX_MODE_ID; - this.providerOptions = CodexProviderOptionsSchema.parse(config.providerOptions ?? {}); + this.providerOptions = + validateProviderOptions("codex", CodexProviderOptionsSchema, config.providerOptions) ?? {}; this.config = config; this.asyncQuestions = new CodexAsyncQuestions(resumeHandle?.metadata?.asyncQuestions); this.codexHome = deps.codexHome ?? resolveCodexHomeDir(process.env); @@ -4902,7 +4904,6 @@ export class CodexAppServerAgentSession implements AgentSession { modeId: this.config.modeId, model: this.config.model ?? null, thinkingOptionId, - providerOptions: this.config.providerOptions, toolPolicy: this.config.toolPolicy, systemPrompt: this.config.systemPrompt, mcpServers: this.config.mcpServers, diff --git a/packages/server/src/server/agent/providers/cursor-acp-agent.ts b/packages/server/src/server/agent/providers/cursor-acp-agent.ts index 0a7ce428b3..0b5b9ac4ec 100644 --- a/packages/server/src/server/agent/providers/cursor-acp-agent.ts +++ b/packages/server/src/server/agent/providers/cursor-acp-agent.ts @@ -16,7 +16,6 @@ interface CursorACPAgentClientOptions { env?: Record; providerId?: string; label?: string; - providerParams?: unknown; } const CURSOR_INITIAL_COMMANDS_WAIT_TIMEOUT_MS = 10_000; @@ -93,7 +92,6 @@ export class CursorACPAgentClient extends GenericACPAgentClient { env: options.env, providerId: options.providerId, label: options.label, - providerParams: options.providerParams, // cursor-agent publishes slash commands asynchronously via available_commands_update. waitForInitialCommands: true, initialCommandsWaitTimeoutMs: CURSOR_INITIAL_COMMANDS_WAIT_TIMEOUT_MS, diff --git a/packages/server/src/server/agent/providers/generic-acp-agent.diagnostic.test.ts b/packages/server/src/server/agent/providers/generic-acp-agent.diagnostic.test.ts index b37e1a9a54..d70117d57d 100644 --- a/packages/server/src/server/agent/providers/generic-acp-agent.diagnostic.test.ts +++ b/packages/server/src/server/agent/providers/generic-acp-agent.diagnostic.test.ts @@ -1,3 +1,4 @@ +import { buildProviderRegistry } from "../provider-registry.js"; import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; @@ -254,22 +255,28 @@ describe("GenericACPAgentClient diagnostics", () => { test("sends configured client capabilities in catalog and live session initialization", async () => { await withFakeACPAgent("success", async (scriptPath, mode, testDir) => { const initializeTracePath = path.join(testDir, "initialize.jsonl"); - const client = new GenericACPAgentClient({ - logger: createTestLogger(), - command: [process.execPath, scriptPath, mode, "", initializeTracePath], - providerParams: { - clientCapabilities: { - fs: { - readTextFile: true, - writeTextFile: true, + const logger = createTestLogger(); + const registry = buildProviderRegistry(logger, { + providerOverrides: { + "configured-acp": { + extends: "acp", + label: "Configured ACP", + command: [process.execPath, scriptPath, mode, "", initializeTracePath], + options: { + supportsMcpServers: false, + clientCapabilities: { + fs: { readTextFile: true, writeTextFile: true }, + terminal: true, + }, }, - terminal: true, }, }, }); + const client = registry["configured-acp"].createClient(logger); await client.fetchCatalog({ scope: "workspace", cwd: testDir, force: true }); - const session = await client.createSession({ provider: "acp", cwd: testDir }); + const session = await client.createSession({ provider: "configured-acp", cwd: testDir }); + expect(session.capabilities.supportsMcpServers).toBe(false); await session.close(); const initializeRequests = parseInitializeTrace(await readFile(initializeTracePath, "utf8")); diff --git a/packages/server/src/server/agent/providers/generic-acp-agent.test.ts b/packages/server/src/server/agent/providers/generic-acp-agent.test.ts index c266cc148b..a1f5c497ec 100644 --- a/packages/server/src/server/agent/providers/generic-acp-agent.test.ts +++ b/packages/server/src/server/agent/providers/generic-acp-agent.test.ts @@ -66,21 +66,4 @@ describe("GenericACPAgentClient", () => { }, ]); }); - - test("uses provider params to report MCP support", () => { - const _client = new GenericACPAgentClient({ - logger: createTestLogger(), - command: ["no-mcp-acp", "serve"], - providerParams: { - supportsMcpServers: false, - }, - }); - void _client; - - expect(mockState.superConstructorOptions.at(-1)).toMatchObject({ - capabilities: { - supportsMcpServers: false, - }, - }); - }); }); diff --git a/packages/server/src/server/agent/providers/generic-acp-agent.ts b/packages/server/src/server/agent/providers/generic-acp-agent.ts index c8f8960ea2..5a14d99137 100644 --- a/packages/server/src/server/agent/providers/generic-acp-agent.ts +++ b/packages/server/src/server/agent/providers/generic-acp-agent.ts @@ -1,7 +1,5 @@ import type { Logger } from "pino"; -import { z } from "zod"; -import type { AgentCapabilityFlags } from "../agent-sdk-types.js"; import { checkProviderLaunchAvailable, resolveProviderLaunch } from "../provider-launch-config.js"; import { ACPAgentClient, @@ -18,32 +16,12 @@ import { toDiagnosticErrorMessage, } from "./diagnostic-utils.js"; -export const GenericACPProviderParamsSchema = z - .object({ - supportsMcpServers: z.boolean().optional(), - clientCapabilities: z - .object({ - fs: z - .object({ - readTextFile: z.boolean().optional(), - writeTextFile: z.boolean().optional(), - }) - .optional(), - terminal: z.boolean().optional(), - }) - .optional(), - }) - .passthrough(); - -type GenericACPProviderParams = z.infer; - interface GenericACPAgentClientOptions { logger: Logger; command: [string, ...string[]]; env?: Record; providerId?: string; label?: string; - providerParams?: unknown; waitForInitialCommands?: boolean; initialCommandsWaitTimeoutMs?: number; diagnosticPhaseTimeoutMs?: number; @@ -61,7 +39,6 @@ export class GenericACPAgentClient extends ACPAgentClient { private readonly diagnosticPhaseTimeoutMs?: number; constructor(options: GenericACPAgentClientOptions) { - const providerParams = parseGenericACPProviderParams(options.providerParams); super({ provider: "acp", logger: options.logger, @@ -69,12 +46,11 @@ export class GenericACPAgentClient extends ACPAgentClient { env: options.env, }, defaultCommand: options.command, - capabilities: buildGenericACPCapabilities(providerParams), + capabilities: DEFAULT_ACP_CAPABILITIES, // ACP agents advertise slash commands with available_commands_update after // session/new, so the first listCommands() waits for that batch. waitForInitialCommands: options.waitForInitialCommands ?? true, initialCommandsWaitTimeoutMs: options.initialCommandsWaitTimeoutMs, - clientCapabilities: providerParams.clientCapabilities, clientCapabilityMeta: options.clientCapabilityMeta, configFeatureOptions: options.configFeatureOptions, extensionCommandsParser: options.extensionCommandsParser, @@ -165,17 +141,6 @@ export class GenericACPAgentClient extends ACPAgentClient { } } -function buildGenericACPCapabilities(params: GenericACPProviderParams): AgentCapabilityFlags { - return { - ...DEFAULT_ACP_CAPABILITIES, - supportsMcpServers: params.supportsMcpServers ?? DEFAULT_ACP_CAPABILITIES.supportsMcpServers, - }; -} - -function parseGenericACPProviderParams(params: unknown): GenericACPProviderParams { - return GenericACPProviderParamsSchema.parse(params ?? {}); -} - export interface CommandInvocation { command: string; args: string[]; diff --git a/packages/server/src/server/agent/providers/kimi-acp-agent.ts b/packages/server/src/server/agent/providers/kimi-acp-agent.ts index 50effdf33a..7ae144f653 100644 --- a/packages/server/src/server/agent/providers/kimi-acp-agent.ts +++ b/packages/server/src/server/agent/providers/kimi-acp-agent.ts @@ -15,7 +15,6 @@ interface KimiACPAgentClientOptions { env?: Record; providerId?: string; label?: string; - providerParams?: unknown; } // Kimi exposes thinking options only for the selected model. Keep its model-switching @@ -88,7 +87,6 @@ export class KimiACPAgentClient extends GenericACPAgentClient { env: options.env, providerId: options.providerId, label: options.label, - providerParams: options.providerParams, catalogModelResolver: resolveKimiCatalogModels, }); } diff --git a/packages/server/src/server/agent/providers/kiro-acp-agent.ts b/packages/server/src/server/agent/providers/kiro-acp-agent.ts index 53fd7f2408..41bbd97647 100644 --- a/packages/server/src/server/agent/providers/kiro-acp-agent.ts +++ b/packages/server/src/server/agent/providers/kiro-acp-agent.ts @@ -10,7 +10,6 @@ interface KiroACPAgentClientOptions { env?: Record; providerId?: string; label?: string; - providerParams?: unknown; } // Kiro CLI publishes its slash commands and skills asynchronously through the @@ -92,7 +91,6 @@ export class KiroACPAgentClient extends GenericACPAgentClient { env: options.env, providerId: options.providerId, label: options.label, - providerParams: options.providerParams, waitForInitialCommands: true, initialCommandsWaitTimeoutMs: KIRO_INITIAL_COMMANDS_WAIT_TIMEOUT_MS, extensionCommandsParser: parseKiroExtensionCommands, diff --git a/packages/server/src/server/agent/providers/omp/agent.test.ts b/packages/server/src/server/agent/providers/omp/agent.test.ts index 90b82cff4b..3841f8bd85 100644 --- a/packages/server/src/server/agent/providers/omp/agent.test.ts +++ b/packages/server/src/server/agent/providers/omp/agent.test.ts @@ -9,7 +9,7 @@ import type { PaseoToolCatalog } from "../../tools/types.js"; import type { OmpAgentMessage } from "./rpc-types.js"; import type { OmpNoTurnScheduler, OmpProviderIdleScheduler } from "./agent.js"; import type { OmpUsagePollScheduler } from "./usage-poller.js"; -import { resolveOmpProviderParams } from "./provider-config.js"; +import { resolveOmpProviderOptions } from "./provider-config.js"; import { OmpRuntimeEventSchema } from "./rpc-types.js"; import { OmpHarness } from "./test-utils/omp-harness.js"; import { OmpAgentClient } from "./agent.js"; @@ -40,11 +40,11 @@ const ABORTED_TERMINAL_RESPONSE: OmpAgentMessage = { }; test("OMP ready timeout defaults to 20 seconds and RPC timeout overrides both", () => { - expect(resolveOmpProviderParams({}).runtimeProviderParams).toMatchObject({ + expect(resolveOmpProviderOptions({}).runtimeOptions).toMatchObject({ readyTimeoutMs: 20_000, rpcTimeoutMs: 60_000, }); - expect(resolveOmpProviderParams({ rpcTimeoutMs: 90_000 }).runtimeProviderParams).toMatchObject({ + expect(resolveOmpProviderOptions({ rpcTimeoutMs: 90_000 }).runtimeOptions).toMatchObject({ readyTimeoutMs: 90_000, rpcTimeoutMs: 90_000, }); diff --git a/packages/server/src/server/agent/providers/omp/agent.ts b/packages/server/src/server/agent/providers/omp/agent.ts index 4e700df275..089376c8ca 100644 --- a/packages/server/src/server/agent/providers/omp/agent.ts +++ b/packages/server/src/server/agent/providers/omp/agent.ts @@ -63,10 +63,9 @@ import { mergeOmpRuntimeSettings, resolveOmpDiagnosticPaths, resolveOmpLaunchMode, - resolveOmpProviderParams, + resolveOmpProviderOptions, OMP_MODES, - type OmpModelRoleParams, - type OmpRuntimeProviderParams, + type OmpRuntimeOptions, } from "./provider-config.js"; export { formatOmpVersionSupport, resolveOmpDiagnosticPaths } from "./provider-config.js"; import { OmpSubagentCardTracker, type OmpSubagentCardScheduler } from "./subagent-card-tracker.js"; @@ -133,7 +132,6 @@ const OMP_CORE_CAPABILITIES: AgentCapabilityFlags = { export interface OmpAgentClientOptions { logger: Logger; runtimeSettings?: ProviderRuntimeSettings; - providerParams?: unknown; runtime?: OmpRuntime; subagentCardScheduler?: OmpSubagentCardScheduler; providerIdleScheduler?: OmpProviderIdleScheduler; @@ -591,15 +589,15 @@ function isOmpAgentSessionEvent(event: OmpRuntimeEvent): event is OmpAgentSessio function createRuntime( logger: Logger, runtimeSettings: ProviderRuntimeSettings | undefined, - providerParams: OmpRuntimeProviderParams, + providerOptions: OmpRuntimeOptions, ): OmpRuntime { return new OmpCliRuntime({ logger, runtimeSettings, command: ["omp"], commandsRpcName: "get_available_commands", - readyTimeoutMs: providerParams.readyTimeoutMs, - requestTimeoutMs: providerParams.rpcTimeoutMs, + readyTimeoutMs: providerOptions.readyTimeoutMs, + requestTimeoutMs: providerOptions.rpcTimeoutMs, }); } @@ -2201,19 +2199,14 @@ export class OmpAgentClient implements AgentClient { private readonly logger: Logger; private readonly runtimeSettings?: ProviderRuntimeSettings; - private readonly providerParams: OmpRuntimeProviderParams; - private readonly modelRoleParams: OmpModelRoleParams; private readonly subagentCardScheduler?: OmpSubagentCardScheduler; private readonly providerIdleScheduler?: OmpProviderIdleScheduler; private readonly noTurnScheduler?: OmpNoTurnScheduler; private readonly usagePollScheduler?: OmpUsagePollScheduler; private readonly providerIdleDeadlineMs?: number; - private readonly runtime: OmpRuntime; + private readonly runtime?: OmpRuntime; constructor(options: OmpAgentClientOptions) { - const { runtimeProviderParams, modelRoleParams } = resolveOmpProviderParams( - options.providerParams, - ); const runtimeSettings = mergeOmpRuntimeSettings( { command: { @@ -2225,15 +2218,12 @@ export class OmpAgentClient implements AgentClient { ); this.logger = options.logger; this.runtimeSettings = runtimeSettings; - this.providerParams = runtimeProviderParams; - this.modelRoleParams = modelRoleParams; this.subagentCardScheduler = options.subagentCardScheduler; this.providerIdleScheduler = options.providerIdleScheduler; this.noTurnScheduler = options.noTurnScheduler; this.usagePollScheduler = options.usagePollScheduler; this.providerIdleDeadlineMs = options.providerIdleDeadlineMs; - this.runtime = - options.runtime ?? createRuntime(options.logger, runtimeSettings, this.providerParams); + this.runtime = options.runtime; } private async configureNativePaseoTools( @@ -2276,7 +2266,7 @@ export class OmpAgentClient implements AgentClient { config: AgentSessionConfig, launchContext?: AgentLaunchContext, ): Promise { - const launchMode = this.resolveLaunchMode(config.modeId); + const launchMode = this.resolveLaunchMode(config.modeId, config.providerOptions); const startInput: OmpStartSessionInput = { cwd: config.cwd, protocolMode: "rpc-ui", @@ -2288,7 +2278,9 @@ export class OmpAgentClient implements AgentClient { systemPrompt: composeSystemPromptParts(config.systemPrompt, config.daemonAppendSystemPrompt), env: launchContext?.env, }; - const runtimeSession = await this.runtime.startSession(startInput); + const runtimeSession = await this.resolveRuntime(config.providerOptions).startSession( + startInput, + ); let hostTools: OmpHostToolRouter | undefined; try { hostTools = await this.configureNativePaseoTools( @@ -2347,14 +2339,19 @@ export class OmpAgentClient implements AgentClient { ); } - const launchMode = this.resolveLaunchMode(resumeConfig.modeId); + const launchMode = this.resolveLaunchMode( + resumeConfig.modeId, + resumeConfig.config.providerOptions, + ); const startInput = buildResumeStartInput({ resumeConfig, sessionFile, launchContext, launchMode, }); - const runtimeSession = await this.runtime.startSession(startInput); + const runtimeSession = await this.resolveRuntime( + resumeConfig.config.providerOptions, + ).startSession(startInput); let hostTools: OmpHostToolRouter | undefined; try { hostTools = await this.configureNativePaseoTools( @@ -2396,8 +2393,8 @@ export class OmpAgentClient implements AgentClient { launchContext?: AgentLaunchContext, ): OmpAgentSessionOptions["restartRuntime"] { return async (sessionFile, modeId) => { - const launchMode = this.resolveLaunchMode(modeId); - const next = await this.runtime.startSession({ + const launchMode = this.resolveLaunchMode(modeId, config.providerOptions); + const next = await this.resolveRuntime(config.providerOptions).startSession({ ...startInput, model: config.model, thinkingOptionId: normalizeOmpThinkingOption(config.thinkingOptionId) ?? undefined, @@ -2428,7 +2425,7 @@ export class OmpAgentClient implements AgentClient { options: FetchCatalogOptions, context?: ProviderRefreshContext, ): Promise { - const launchMode = this.resolveLaunchMode(undefined); + const launchMode = this.resolveLaunchMode(undefined, options.providerOptions); let runtimeSession: OmpRuntimeSession | undefined; let closePromise: Promise | undefined; const closeSession = () => { @@ -2440,7 +2437,7 @@ export class OmpAgentClient implements AgentClient { context?.signal.addEventListener("abort", handleAbort, { once: true }); try { await runProviderRefreshActivity(context, "runtime.start", async () => { - runtimeSession = await this.runtime.startSession({ + runtimeSession = await this.resolveRuntime(options.providerOptions).startSession({ cwd: options.scope === "global" ? homedir() : options.cwd, protocolMode: "rpc-ui", modeId: launchMode.modeId, @@ -2474,14 +2471,15 @@ export class OmpAgentClient implements AgentClient { ): Promise { return await listOmpImportableSessions({ ...options, - sessionDir: this.providerParams.sessionDir, + sessionDir: resolveOmpProviderOptions(options?.providerOptions).runtimeOptions.sessionDir, runtimeSettings: this.runtimeSettings, }); } async importSession(input: ImportProviderSessionInput, context: ImportProviderSessionContext) { const descriptorOptions = { - sessionDir: this.providerParams.sessionDir, + sessionDir: resolveOmpProviderOptions(context.config.providerOptions).runtimeOptions + .sessionDir, runtimeSettings: this.runtimeSettings, }; const importConfig = await readOmpImportSessionConfig( @@ -2556,11 +2554,19 @@ export class OmpAgentClient implements AgentClient { } } - private resolveLaunchMode(modeId: string | undefined): { + private resolveLaunchMode( + modeId: string | undefined, + providerOptions?: Record, + ): { modeId: string; extraArgs: string[]; } { - return resolveOmpLaunchMode(modeId, this.modelRoleParams); + return resolveOmpLaunchMode(modeId, resolveOmpProviderOptions(providerOptions).modelRoles); + } + + private resolveRuntime(providerOptions: Record | undefined): OmpRuntime { + const { runtimeOptions } = resolveOmpProviderOptions(providerOptions); + return this.runtime ?? createRuntime(this.logger, this.runtimeSettings, runtimeOptions); } private async resolveOmpLaunch(): Promise { diff --git a/packages/server/src/server/agent/providers/omp/provider-config.ts b/packages/server/src/server/agent/providers/omp/provider-config.ts index 2fe5067845..0bcd369a1c 100644 --- a/packages/server/src/server/agent/providers/omp/provider-config.ts +++ b/packages/server/src/server/agent/providers/omp/provider-config.ts @@ -13,7 +13,7 @@ const DEFAULT_OMP_RPC_TIMEOUT_MS = 60_000; export const MIN_SUPPORTED_OMP_VERSION = "16.3.9"; export { OMP_MODES }; -export const OmpProviderParamsSchema = z +export const OmpProviderOptionsSchema = z .object({ sessionDir: z.string().min(1).optional(), rpcTimeoutMs: z.number().int().positive().optional(), @@ -23,13 +23,13 @@ export const OmpProviderParamsSchema = z }) .strict(); -export interface OmpRuntimeProviderParams { +export interface OmpRuntimeOptions { sessionDir?: string; readyTimeoutMs: number; rpcTimeoutMs: number; } -export interface OmpModelRoleParams { +export interface OmpModelRoles { smolModel?: string; slowModel?: string; planModel?: string; @@ -37,9 +37,9 @@ export interface OmpModelRoleParams { export function resolveOmpLaunchMode( modeId: string | undefined, - modelRoleParams: OmpModelRoleParams = {}, + modelRoles: OmpModelRoles = {}, ): { modeId: string; extraArgs: string[] } { - const modelRoleArgs = resolveOmpModelRoleArgs(modelRoleParams); + const modelRoleArgs = resolveOmpModelRoleArgs(modelRoles); switch (modeId ?? DEFAULT_OMP_MODE_ID) { case "full": return { modeId: "full", extraArgs: ["--approval-mode", "yolo", ...modelRoleArgs] }; @@ -58,11 +58,11 @@ export function resolveOmpLaunchMode( } } -function resolveOmpModelRoleArgs(modelRoleParams: OmpModelRoleParams): string[] { +function resolveOmpModelRoleArgs(modelRoles: OmpModelRoles): string[] { const args: string[] = []; - if (modelRoleParams.smolModel) args.push("--smol", modelRoleParams.smolModel); - if (modelRoleParams.slowModel) args.push("--slow", modelRoleParams.slowModel); - if (modelRoleParams.planModel) args.push("--plan", modelRoleParams.planModel); + if (modelRoles.smolModel) args.push("--smol", modelRoles.smolModel); + if (modelRoles.slowModel) args.push("--slow", modelRoles.slowModel); + if (modelRoles.planModel) args.push("--plan", modelRoles.planModel); return args; } @@ -128,22 +128,22 @@ export function formatOmpVersionSupport(versionOutput: string): string { return `${match[1]}.${match[2]}.${match[3]} (${supported ? "supported" : "unsupported"}; minimum ${MIN_SUPPORTED_OMP_VERSION})`; } -export function resolveOmpProviderParams(providerParams: unknown): { - runtimeProviderParams: OmpRuntimeProviderParams; - modelRoleParams: OmpModelRoleParams; +export function resolveOmpProviderOptions(providerOptions: unknown): { + runtimeOptions: OmpRuntimeOptions; + modelRoles: OmpModelRoles; } { - const params = OmpProviderParamsSchema.parse(providerParams ?? {}); - const configuredRpcTimeoutMs = params.rpcTimeoutMs; + const options = OmpProviderOptionsSchema.parse(providerOptions ?? {}); + const configuredRpcTimeoutMs = options.rpcTimeoutMs; return { - runtimeProviderParams: { - sessionDir: params.sessionDir, + runtimeOptions: { + sessionDir: options.sessionDir, readyTimeoutMs: configuredRpcTimeoutMs ?? DEFAULT_OMP_READY_TIMEOUT_MS, rpcTimeoutMs: configuredRpcTimeoutMs ?? DEFAULT_OMP_RPC_TIMEOUT_MS, }, - modelRoleParams: { - ...(params.smolModel ? { smolModel: params.smolModel } : {}), - ...(params.slowModel ? { slowModel: params.slowModel } : {}), - ...(params.planModel ? { planModel: params.planModel } : {}), + modelRoles: { + ...(options.smolModel ? { smolModel: options.smolModel } : {}), + ...(options.slowModel ? { slowModel: options.slowModel } : {}), + ...(options.planModel ? { planModel: options.planModel } : {}), }, }; } diff --git a/packages/server/src/server/agent/providers/opencode-agent.ts b/packages/server/src/server/agent/providers/opencode-agent.ts index 70f7f367e1..ccd605ef06 100644 --- a/packages/server/src/server/agent/providers/opencode-agent.ts +++ b/packages/server/src/server/agent/providers/opencode-agent.ts @@ -1,3 +1,4 @@ +import { validateProviderOptions } from "../provider-options.js"; import { createOpencodeClient, type AssistantMessage as OpenCodeAssistantMessage, @@ -1930,7 +1931,9 @@ export class OpenCodeAgentClient implements AgentClient { if (config.provider !== "opencode") { throw new Error(`OpenCodeAgentClient received config for provider '${config.provider}'`); } - const providerOptions = OpenCodeProviderOptionsSchema.parse(config.providerOptions ?? {}); + const providerOptions = + validateProviderOptions("opencode", OpenCodeProviderOptionsSchema, config.providerOptions) ?? + {}; return normalizeOpenCodeConfig({ ...config, provider: "opencode", providerOptions }); } diff --git a/packages/server/src/server/agent/providers/pi/agent.test.ts b/packages/server/src/server/agent/providers/pi/agent.test.ts index 29c515ad76..bf04d54071 100644 --- a/packages/server/src/server/agent/providers/pi/agent.test.ts +++ b/packages/server/src/server/agent/providers/pi/agent.test.ts @@ -18,7 +18,7 @@ import { describe, expect, onTestFinished, test } from "vitest"; import type { AgentSession, AgentSessionConfig, AgentStreamEvent } from "../../agent-sdk-types.js"; import { - PiProviderParamsSchema, + PiProviderOptionsSchema, PiRpcAgentClient, PiRpcAgentSession, transformPiModels, @@ -56,8 +56,8 @@ interface PiThinkingCatalogCase { } test("Pi RPC timeout defaults to 60 seconds and accepts an override", () => { - expect(PiProviderParamsSchema.parse({}).rpcTimeoutMs).toBe(60_000); - expect(PiProviderParamsSchema.parse({ rpcTimeoutMs: 90_000 }).rpcTimeoutMs).toBe(90_000); + expect(PiProviderOptionsSchema.parse({}).rpcTimeoutMs).toBe(60_000); + expect(PiProviderOptionsSchema.parse({ rpcTimeoutMs: 90_000 }).rpcTimeoutMs).toBe(90_000); }); function createClient( @@ -2465,10 +2465,11 @@ describe("PiRpcAgentClient", () => { const client = new PiRpcAgentClient({ logger: pino({ level: "silent" }), runtime: new FakePi(), - providerParams: { sessionDir: sessionsDir }, }); - await expect(client.listImportableSessions({ cwd })).resolves.toEqual([ + await expect( + client.listImportableSessions({ cwd, providerOptions: { sessionDir: sessionsDir } }), + ).resolves.toEqual([ { providerHandleId: sessionFile, cwd, @@ -2516,7 +2517,9 @@ describe("PiRpcAgentClient", () => { }, }); - await expect(client.listImportableSessions({ cwd })).resolves.toMatchObject([ + await expect( + client.listImportableSessions({ cwd, providerOptions: { sessionDir: sessionsDir } }), + ).resolves.toMatchObject([ { providerHandleId: sessionFile, cwd, @@ -2572,7 +2575,6 @@ describe("PiRpcAgentClient", () => { const client = new PiRpcAgentClient({ logger: pino({ level: "silent" }), runtime: pi, - providerParams: { sessionDir: sessionsDir }, }); const imported = await client.importSession( diff --git a/packages/server/src/server/agent/providers/pi/agent.ts b/packages/server/src/server/agent/providers/pi/agent.ts index 1d8b969030..6b5de0314e 100644 --- a/packages/server/src/server/agent/providers/pi/agent.ts +++ b/packages/server/src/server/agent/providers/pi/agent.ts @@ -105,7 +105,7 @@ const DEFAULT_PI_EXTENSION_RESULT_TIMEOUT_MS = 30_000; const DEFAULT_PI_RPC_TIMEOUT_MS = 60_000; const QUESTION_RESPONSE_HEADER = "Response"; -export const PiProviderParamsSchema = z +export const PiProviderOptionsSchema = z .object({ sessionDir: z.string().min(1).optional(), rpcTimeoutMs: z.number().int().positive().default(DEFAULT_PI_RPC_TIMEOUT_MS), @@ -113,8 +113,6 @@ export const PiProviderParamsSchema = z }) .strict(); -type PiProviderParams = z.infer; - const PI_HANDLED_BUILTIN_SLASH_COMMANDS: AgentSlashCommand[] = [ { name: "compact", @@ -186,7 +184,6 @@ const PI_THINKING_OPTIONS: ReadonlyArray<{ export interface PiRpcAgentClientOptions { logger: Logger; runtimeSettings?: ProviderRuntimeSettings; - providerParams?: unknown; runtime?: PiRuntime; usagePollScheduler?: PiUsagePollScheduler; } @@ -2465,8 +2462,7 @@ export class PiRpcAgentClient implements AgentClient { private readonly logger: Logger; private readonly runtimeSettings?: ProviderRuntimeSettings; - private readonly providerParams: PiProviderParams; - private readonly runtime: PiRuntime; + private readonly runtime?: PiRuntime; private readonly usagePollScheduler?: PiUsagePollScheduler; constructor(options: PiRpcAgentClientOptions) { @@ -2474,10 +2470,7 @@ export class PiRpcAgentClient implements AgentClient { this.capabilities = capabilitiesForClient(); this.logger = options.logger; this.runtimeSettings = options.runtimeSettings; - this.providerParams = PiProviderParamsSchema.parse(options.providerParams ?? {}); - this.runtime = - options.runtime ?? - createRuntime(options.logger, options.runtimeSettings, this.providerParams.rpcTimeoutMs); + this.runtime = options.runtime; this.usagePollScheduler = options.usagePollScheduler; } @@ -2485,11 +2478,13 @@ export class PiRpcAgentClient implements AgentClient { config: AgentSessionConfig, launchContext?: AgentLaunchContext, ): Promise { + const providerOptions = PiProviderOptionsSchema.parse(config.providerOptions ?? {}); + const runtime = this.resolveRuntime(providerOptions.rpcTimeoutMs); const mcpEnv = { ...this.runtimeSettings?.env, ...launchContext?.env, }; - const mcp = await this.prepareMcpInjection(config.cwd, config.mcpServers, mcpEnv); + const mcp = await this.prepareMcpInjection(config.cwd, config.mcpServers, mcpEnv, runtime); const mcpConfigFile = mcp?.kind === "adapter" ? mcp.configFile : null; const paseoExtension = createPiPaseoExtensionFile({ systemPrompt: composeSystemPromptParts(config.systemPrompt, config.daemonAppendSystemPrompt), @@ -2497,7 +2492,7 @@ export class PiRpcAgentClient implements AgentClient { }); let runtimeSession: PiRuntimeSession; try { - runtimeSession = await this.runtime.startSession({ + runtimeSession = await runtime.startSession({ cwd: config.cwd, model: config.model, thinkingOptionId: normalizePiThinkingOption(config.thinkingOptionId) ?? undefined, @@ -2518,7 +2513,7 @@ export class PiRpcAgentClient implements AgentClient { initialState: await runtimeSession.getState(), capabilities: capabilitiesForSession(mcp !== null), cleanup: combineCleanup([mcpConfigFile?.cleanup, paseoExtension?.cleanup]), - extensionTimeoutMs: this.providerParams.extensionTimeoutMs, + extensionTimeoutMs: providerOptions.extensionTimeoutMs, logger: this.logger, usagePollScheduler: this.usagePollScheduler, }); @@ -2543,6 +2538,10 @@ export class PiRpcAgentClient implements AgentClient { const persistenceMetadata = parsePersistenceMetadata(handle.metadata); const resumeConfig = buildResumeConfig(persistenceMetadata, overrides, this.provider); + const providerOptions = PiProviderOptionsSchema.parse( + resumeConfig.config.providerOptions ?? {}, + ); + const runtime = this.resolveRuntime(providerOptions.rpcTimeoutMs); const mcpEnv = { ...this.runtimeSettings?.env, ...launchContext?.env, @@ -2551,6 +2550,7 @@ export class PiRpcAgentClient implements AgentClient { resumeConfig.cwd, resumeConfig.config.mcpServers, mcpEnv, + runtime, ); const mcpConfigFile = mcp?.kind === "adapter" ? mcp.configFile : null; const paseoExtension = createPiPaseoExtensionFile({ @@ -2562,7 +2562,7 @@ export class PiRpcAgentClient implements AgentClient { }); let runtimeSession: PiRuntimeSession; try { - runtimeSession = await this.runtime.startSession( + runtimeSession = await runtime.startSession( buildResumeStartInput({ resumeConfig, sessionFile, @@ -2583,7 +2583,7 @@ export class PiRpcAgentClient implements AgentClient { initialState: await runtimeSession.getState(), capabilities: capabilitiesForSession(mcp !== null), cleanup: combineCleanup([mcpConfigFile?.cleanup, paseoExtension?.cleanup]), - extensionTimeoutMs: this.providerParams.extensionTimeoutMs, + extensionTimeoutMs: providerOptions.extensionTimeoutMs, logger: this.logger, usagePollScheduler: this.usagePollScheduler, }); @@ -2599,6 +2599,8 @@ export class PiRpcAgentClient implements AgentClient { options: FetchCatalogOptions, context?: ProviderRefreshContext, ): Promise { + const providerOptions = PiProviderOptionsSchema.parse(options.providerOptions ?? {}); + const runtime = this.resolveRuntime(providerOptions.rpcTimeoutMs); let runtimeSession: PiRuntimeSession | undefined; let closePromise: Promise | undefined; const closeSession = () => { @@ -2610,7 +2612,7 @@ export class PiRpcAgentClient implements AgentClient { context?.signal.addEventListener("abort", handleAbort, { once: true }); try { await runProviderRefreshActivity(context, "runtime.start", async () => { - runtimeSession = await this.runtime.startSession({ + runtimeSession = await runtime.startSession({ cwd: options.scope === "global" ? homedir() : options.cwd, signal: context?.signal, }); @@ -2652,7 +2654,7 @@ export class PiRpcAgentClient implements AgentClient { ): Promise { return await listPiImportableSessions({ ...options, - sessionDir: this.providerParams.sessionDir, + sessionDir: PiProviderOptionsSchema.parse(options?.providerOptions ?? {}).sessionDir, runtimeSettings: this.runtimeSettings, }); } @@ -2704,15 +2706,20 @@ export class PiRpcAgentClient implements AgentClient { } } + private resolveRuntime(rpcTimeoutMs: number): PiRuntime { + return this.runtime ?? createRuntime(this.logger, this.runtimeSettings, rpcTimeoutMs); + } + private async prepareMcpInjection( cwd: string, servers: Record | undefined, env: Record | undefined, + runtime: PiRuntime, ): Promise { if (!servers || Object.keys(servers).length === 0) { return null; } - const mcpSupport = await this.detectMcpSupport(cwd, env); + const mcpSupport = await this.detectMcpSupport(cwd, env, runtime); if (mcpSupport === "builtin") { return { kind: "builtin", servers: toPiBuiltinMcpServers(servers) }; } @@ -2727,9 +2734,10 @@ export class PiRpcAgentClient implements AgentClient { private async detectMcpSupport( cwd: string, - env?: Record, + env: Record | undefined, + runtime: PiRuntime, ): Promise { - const runtimeSession = await this.runtime.startSession({ cwd, env }).catch((error) => { + const runtimeSession = await runtime.startSession({ cwd, env }).catch((error) => { this.logger.debug({ err: error, cwd }, "Pi MCP probe failed to start"); return null; }); diff --git a/packages/server/src/server/agent/providers/trae-acp-agent.ts b/packages/server/src/server/agent/providers/trae-acp-agent.ts index b1e0893e4b..c14525211e 100644 --- a/packages/server/src/server/agent/providers/trae-acp-agent.ts +++ b/packages/server/src/server/agent/providers/trae-acp-agent.ts @@ -8,7 +8,6 @@ interface TraeACPAgentClientOptions { env?: Record; providerId?: string; label?: string; - providerParams?: unknown; } const TRAE_INITIAL_COMMANDS_WAIT_TIMEOUT_MS = 10_000; @@ -21,7 +20,6 @@ export class TraeACPAgentClient extends GenericACPAgentClient { env: options.env, providerId: options.providerId, label: options.label, - providerParams: options.providerParams, // traecli publishes slash commands and skills asynchronously via available_commands_update. waitForInitialCommands: true, initialCommandsWaitTimeoutMs: TRAE_INITIAL_COMMANDS_WAIT_TIMEOUT_MS, diff --git a/packages/server/src/server/hub/daemon-executions.test.ts b/packages/server/src/server/hub/daemon-executions.test.ts index a96dc8cfdd..d78b29d732 100644 --- a/packages/server/src/server/hub/daemon-executions.test.ts +++ b/packages/server/src/server/hub/daemon-executions.test.ts @@ -1,3 +1,7 @@ +import type { AgentClient } from "../agent/agent-sdk-types.js"; +import { validateProviderOptions } from "../agent/provider-options.js"; +import { CodexProviderOptionsSchema } from "../agent/providers/codex/options.js"; +import { createTestAgentClients } from "../test-utils/fake-agent-client.js"; import { afterEach, expect, test } from "vitest"; import { HubRelationshipHarness } from "./test-utils/relationship-harness.js"; @@ -8,8 +12,12 @@ afterEach(async () => { relationship = null; }); -async function launchRelationship(): Promise { - const launched = await HubRelationshipHarness.start(); +async function launchRelationship( + clients?: Record, +): Promise { + const launched = clients + ? await HubRelationshipHarness.startWithRealAgentClients(clients) + : await HubRelationshipHarness.start(); await launched.beginConnect().result; launched.connectLatestSocket(); relationship = launched; @@ -127,8 +135,22 @@ test("Hub can preapprove only tools on MCP servers injected in the same request" expect(hub.providerCreations()).toBe(0); }); -test("Hub returns path-specific structured provider option feedback", async () => { - const hub = await launchRelationship(); +test("Hub returns path-specific feedback thrown by the provider", async () => { + const clients = createTestAgentClients(); + const inner = clients.codex; + const provider: AgentClient = { + provider: inner.provider, + capabilities: inner.capabilities, + isAvailable: (signal, options) => inner.isAvailable(signal, options), + fetchCatalog: (options, context) => inner.fetchCatalog(options, context), + async createSession(config, launch, options) { + validateProviderOptions("codex", CodexProviderOptionsSchema, config.providerOptions); + return inner.createSession(config, launch, options); + }, + resumeSession: (handle, overrides, launch, options) => + inner.resumeSession(handle, overrides, launch, options), + }; + const hub = await launchRelationship({ ...clients, codex: provider }); hub.beginOwnedCreate("invalid-options", "invalid-options-execution", { providerOptions: { sandbox_workspace_write: { writable_roots: ["/tmp", 42] }, diff --git a/packages/server/src/server/schedule/service.test.ts b/packages/server/src/server/schedule/service.test.ts index 612bb7e9a9..14e0221913 100644 --- a/packages/server/src/server/schedule/service.test.ts +++ b/packages/server/src/server/schedule/service.test.ts @@ -21,8 +21,6 @@ import type { AgentStreamEvent, } from "../agent/agent-sdk-types.js"; import { createTestAgentClients } from "../test-utils/fake-agent-client.js"; -import { validateProviderOptions } from "../agent/provider-options.js"; -import { ClaudeProviderOptionsSchema } from "../agent/providers/claude/options.js"; import { createTestLogger } from "../../test-utils/test-logger.js"; import type { ProviderSnapshotManager } from "../agent/provider-snapshot-manager.js"; import { createWorkspaceProvisioningService } from "../session/workspace-provisioning/workspace-provisioning-service.js"; @@ -66,12 +64,6 @@ const NO_UNATTENDED_SCHEDULE_POLICY: Pick - validateProviderOptions("claude", ClaudeProviderOptionsSchema, options), - applyOptions: (config: AgentSessionConfig, options: AgentSessionConfig["providerOptions"]) => ({ - ...config, - ...(options ? { providerOptions: options } : {}), - }), }; let workspaceArchiveInProgress = false; diff --git a/public-docs/custom-providers.md b/public-docs/custom-providers.md index b325492768..8cc80add18 100644 --- a/public-docs/custom-providers.md +++ b/public-docs/custom-providers.md @@ -24,6 +24,25 @@ To override a bundled provider plugin, use its provider ID and omit `extends`. F The examples below are a quick tour. The full, up-to-date reference is on GitHub: [docs/custom-providers.md](https://github.com/getpaseo/paseo/blob/main/docs/custom-providers.md). +## Provider options + +Set defaults in `agents.providers..options`: + +```json +{ + "agents": { + "providers": { + "pi": { "options": { "rpcTimeoutMs": 90000 } } + } + } +} +``` + +Override defaults for one agent with SDK `config.options` (wire +`providerOptions`). The provider validates and applies the resulting record. +See [Provider options](/docs/sdk/provider-options) for the two layers, deep-merge +rule, persistence behavior, and provider-specific examples. + ## Extending a first-class provider ```json @@ -194,4 +213,4 @@ Any agent that speaks [ACP](https://agentclientprotocol.com) over stdio can be a ## Full reference -For the complete field reference (`extends`, `label`, `command`, `env`, `models`, `additionalModels`, `disallowedTools`, `paseoTools`, `enabled`, `order`), model and thinking-option schemas, and deeper examples for each plan, see [docs/custom-providers.md](https://github.com/getpaseo/paseo/blob/main/docs/custom-providers.md) on GitHub. See [Limit Paseo tools by provider](/docs/mcp#limit-paseo-tools-by-provider) for `paseoTools` configuration. +For the complete field reference (`extends`, `label`, `command`, `env`, `options`, `models`, `additionalModels`, `disallowedTools`, `paseoTools`, `enabled`, `order`), model and thinking-option schemas, and deeper examples for each plan, see [docs/custom-providers.md](https://github.com/getpaseo/paseo/blob/main/docs/custom-providers.md) on GitHub. See [Limit Paseo tools by provider](/docs/mcp#limit-paseo-tools-by-provider) for `paseoTools` configuration. diff --git a/public-docs/plugins/reference.md b/public-docs/plugins/reference.md index 4d84edb578..d96b18f9d6 100644 --- a/public-docs/plugins/reference.md +++ b/public-docs/plugins/reference.md @@ -288,7 +288,10 @@ effects. Repeat `clientMessageId` on the live user timeline item and publish exa `session.prompt_result`. Publish provider-created children as sessions with `parentSessionId`. Provider settings are toggle/select descriptors that Paseo renders in the composer. Keep -provider-private JSON under `providerOptions`. Host tools arrive as MCP servers in the complete +provider-private options under `ProviderSessionConfig.providerOptions` on `session.open`. +This record includes configured defaults and per-agent overrides; validate and apply it in your +provider. See [Provider options](/docs/sdk/provider-options) for configuration and merge semantics. +Host tools arrive as MCP servers in the complete session config. Paseo refreshes an agent by closing its current provider session and opening it with current @@ -603,7 +606,7 @@ type PluginTurnOutcome = | `provider`, `model` | Separate fields; changing provider may require changing model/mode/options | | `modeId`, `thinkingOptionId`, `featureValues` | Provider-specific selections | | `title`, `systemPrompt` | Agent configuration | -| `providerOptions` | Provider-specific validated options | +| `providerOptions` | Opaque provider-specific options | | `mcpServers`, `toolPolicy` | MCP configuration and exact-tool preapprovals | | `cwd` | Cannot change | | `internal` | Daemon-owned; cannot change through this hook | diff --git a/public-docs/sdk/provider-options.md b/public-docs/sdk/provider-options.md index a8bd5bad0b..5450890224 100644 --- a/public-docs/sdk/provider-options.md +++ b/public-docs/sdk/provider-options.md @@ -8,9 +8,46 @@ category: TypeScript SDK # Provider options -`config.options` passes settings straight through to the provider CLI. Paseo validates the object against that provider's strict schema before starting the agent, so an unknown or misspelled key fails agent creation instead of silently doing nothing. +Provider options are an opaque `Record` passed to the provider. +The provider decides how to validate and apply it. Claude, Codex, and OpenCode +use strict schemas: an unknown or misspelled key fails agent creation with a +message naming the bad key. Providers without option handling ignore the record. +Pi, OMP, ACP, and plugin providers interpret their own keys. -Options are provider-native. A Codex sandbox key is not a Claude sandbox key. Codex, Claude, and OpenCode accept options; every other provider rejects a non-empty `options`. +You set options in two places: + +- **Provider defaults:** `config.json` → `agents.providers..options`. +- **One agent:** `config.options` in `client.agents.create()`. Its wire name is + `providerOptions`. + +The daemon deep-merges the agent's options over the provider defaults at session +launch or resume. Plain objects merge recursively; arrays, scalars, and `null` +replace the base value. Only the agent's own options are stored. Changing +`config.json` defaults takes effect on the next launch or resume. + +For example, set Codex defaults once: + +```json +{ + "agents": { + "providers": { + "codex": { + "options": { + "sandbox_mode": "read-only", + "approval_policy": "never", + "sandbox_workspace_write": { "network_access": false } + } + } + } + } +} +``` + +Then create one agent with `config.options: { sandbox_mode: "workspace-write" }`. +It receives `workspace-write`, retains `approval_policy: "never"`, and retains +`sandbox_workspace_write.network_access: false`. With no per-agent options it +receives the defaults above. Keys are provider-native; use the sections below +for the options each provider accepts. Provider options are not a host boundary. They constrain the agent CLI, which runs as your user on your machine. For untrusted work, run the daemon in a container or on a separate machine. See [Security](/docs/security). From 9d9d9af993afcefb4bc0f61dbe929bc7267d81f8 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Wed, 30 Sep 2026 19:39:09 +0100 Subject: [PATCH 029/100] Remove the unreleased Antigravity provider plugin (#5781) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor(providers): remove the unreleased Antigravity plugin Remove the provider at the owner’s request because headless command approvals are unsupported. Preserve Muse, shared plugin infrastructure, and earlier editor and terminal integrations. * test(plugins): keep builtin startup coverage generic --- CLAUDE.md | 2 +- README.md | 5 +- docs/providers.md | 9 +- .../src/server/daemon-e2e/models.e2e.test.ts | 58 -- .../src/server/plugins/builtin/index.ts | 1 - .../website/src/components/agent-icons.tsx | 17 - .../website/src/components/landing-page.tsx | 10 +- .../website/src/components/site-footer.tsx | 6 - .../content/alternatives/claude-desktop.md | 42 +- .../src/content/alternatives/codex-app.md | 2 +- .../src/content/alternatives/conductor.md | 34 +- .../src/content/alternatives/happy-coder.md | 34 +- .../src/content/alternatives/openchamber.md | 36 +- .../content/alternatives/opencode-desktop.md | 38 +- .../website/src/content/alternatives/orca.md | 40 +- .../src/content/alternatives/superset.md | 2 +- packages/website/src/data/agent-pages.ts | 10 - packages/website/src/llms.ts | 2 +- packages/website/src/routeTree.gen.ts | 21 - packages/website/src/routes/agents.tsx | 2 +- packages/website/src/routes/antigravity.tsx | 4 - plugins/antigravity-provider/icon.svg | 3 - plugins/antigravity-provider/index.server.ts | 7 - .../antigravity-provider/paseo-plugin.json | 1 - .../server/internal/catalog.ts | 96 --- .../server/internal/process.ts | 224 ------ .../server/internal/prompt.ts | 64 -- .../server/internal/session.ts | 382 ---------- .../server/internal/signals.ts | 40 - .../server/internal/timeline.ts | 55 -- .../server/internal/wire.ts | 85 --- .../server/provider.test.ts | 717 ------------------ .../antigravity-provider/server/provider.ts | 171 ----- plugins/antigravity-provider/test/agy.mjs | 102 --- .../test/fixtures/README.md | 100 --- .../test/fixtures/agents.capture.ndjson | 2 - .../test/fixtures/agents.stderr.ndjson | 0 .../test/fixtures/agents.stdin.ndjson | 0 .../test/fixtures/agents.stdout.ndjson | 0 .../fixtures/api-key-env-only.capture.ndjson | 2 - .../fixtures/api-key-env-only.stderr.ndjson | 2 - .../fixtures/api-key-env-only.stdin.ndjson | 0 .../fixtures/api-key-env-only.stdout.ndjson | 1 - .../fixtures/api-key-settings.capture.ndjson | 2 - .../fixtures/api-key-settings.stderr.ndjson | 1 - .../fixtures/api-key-settings.stdin.ndjson | 1 - .../fixtures/api-key-settings.stdout.ndjson | 2 - .../cwd-without-add-dir.capture.ndjson | 2 - .../cwd-without-add-dir.stderr.ndjson | 0 .../fixtures/cwd-without-add-dir.stdin.ndjson | 1 - .../cwd-without-add-dir.stdout.ndjson | 11 - .../test/fixtures/denial-replay.stdout.ndjson | 11 - .../effort-encoded-match.capture.ndjson | 2 - .../effort-encoded-match.stderr.ndjson | 0 .../effort-encoded-match.stdin.ndjson | 1 - .../effort-encoded-match.stdout.ndjson | 19 - .../effort-encoded-mismatch.capture.ndjson | 3 - .../effort-encoded-mismatch.stderr.ndjson | 1 - .../effort-encoded-mismatch.stdin.ndjson | 0 .../effort-encoded-mismatch.stdout.ndjson | 1 - .../fixtures/effort-mismatch.capture.ndjson | 2 - .../fixtures/effort-mismatch.stderr.ndjson | 1 - .../fixtures/effort-mismatch.stdin.ndjson | 0 .../fixtures/effort-mismatch.stdout.ndjson | 1 - .../effort-unsupported-family.capture.ndjson | 2 - .../effort-unsupported-family.stderr.ndjson | 1 - .../effort-unsupported-family.stdin.ndjson | 0 .../effort-unsupported-family.stdout.ndjson | 1 - .../test/fixtures/help.capture.ndjson | 2 - .../test/fixtures/help.stderr.ndjson | 39 - .../test/fixtures/help.stdin.ndjson | 0 .../test/fixtures/help.stdout.ndjson | 0 .../fixtures/image-content.capture.ndjson | 2 - .../test/fixtures/image-content.stderr.ndjson | 1 - .../test/fixtures/image-content.stdin.ndjson | 1 - .../test/fixtures/image-content.stdout.ndjson | 2 - .../image-file-external.capture.ndjson | 2 - .../test/fixtures/image-file-external.png | Bin 910 -> 0 bytes .../image-file-external.stderr.ndjson | 0 .../fixtures/image-file-external.stdin.ndjson | 1 - .../image-file-external.stdout.ndjson | 14 - .../test/fixtures/mcp-add-http.capture.ndjson | 3 - .../test/fixtures/mcp-add-http.stderr.ndjson | 0 .../test/fixtures/mcp-add-http.stdin.ndjson | 0 .../test/fixtures/mcp-add-http.stdout.ndjson | 1 - .../test/fixtures/mcp-add.capture.ndjson | 2 - .../test/fixtures/mcp-add.stderr.ndjson | 1 - .../test/fixtures/mcp-add.stdin.ndjson | 0 .../test/fixtures/mcp-add.stdout.ndjson | 0 .../mcp-agent-definition.capture.ndjson | 2 - .../mcp-agent-definition.stderr.ndjson | 0 .../mcp-agent-definition.stdin.ndjson | 1 - .../mcp-agent-definition.stdout.ndjson | 15 - .../mcp-agent-discovered.capture.ndjson | 2 - .../mcp-agent-discovered.stderr.ndjson | 0 .../mcp-agent-discovered.stdin.ndjson | 1 - .../mcp-agent-discovered.stdout.ndjson | 19 - .../fixtures/mcp-agent-file.capture.ndjson | 2 - .../fixtures/mcp-agent-file.stderr.ndjson | 0 .../test/fixtures/mcp-agent-file.stdin.ndjson | 1 - .../fixtures/mcp-agent-file.stdout.ndjson | 39 - .../test/fixtures/mcp-global.capture.ndjson | 2 - .../test/fixtures/mcp-global.stderr.ndjson | 0 .../test/fixtures/mcp-global.stdin.ndjson | 1 - .../test/fixtures/mcp-global.stdout.ndjson | 14 - .../test/fixtures/mcp-help.capture.ndjson | 2 - .../test/fixtures/mcp-help.stderr.ndjson | 28 - .../test/fixtures/mcp-help.stdin.ndjson | 0 .../test/fixtures/mcp-help.stdout.ndjson | 0 .../test/fixtures/mcp-http.requests.ndjson | 20 - .../test/fixtures/mcp-list.capture.ndjson | 2 - .../test/fixtures/mcp-list.stderr.ndjson | 0 .../test/fixtures/mcp-list.stdin.ndjson | 0 .../test/fixtures/mcp-list.stdout.ndjson | 1 - .../fixtures/mcp-owned-cwd.capture.ndjson | 2 - .../test/fixtures/mcp-owned-cwd.stderr.ndjson | 0 .../test/fixtures/mcp-owned-cwd.stdin.ndjson | 1 - .../test/fixtures/mcp-owned-cwd.stdout.ndjson | 95 --- .../mcp-owned-first-add-dir.capture.ndjson | 2 - .../mcp-owned-first-add-dir.stderr.ndjson | 0 .../mcp-owned-first-add-dir.stdin.ndjson | 1 - .../mcp-owned-first-add-dir.stdout.ndjson | 60 -- .../mcp-project-config.capture.ndjson | 2 - .../fixtures/mcp-project-config.stderr.ndjson | 0 .../fixtures/mcp-project-config.stdin.ndjson | 1 - .../fixtures/mcp-project-config.stdout.ndjson | 5 - .../mcp-project-isolated.capture.ndjson | 4 - .../mcp-project-isolated.stderr.ndjson | 0 .../mcp-project-isolated.stdin.ndjson | 1 - .../mcp-project-isolated.stdout.ndjson | 31 - .../test/fixtures/mcp-project.capture.ndjson | 2 - .../test/fixtures/mcp-project.stderr.ndjson | 0 .../test/fixtures/mcp-project.stdin.ndjson | 1 - .../test/fixtures/mcp-project.stdout.ndjson | 15 - .../mcp-second-add-dir.capture.ndjson | 2 - .../fixtures/mcp-second-add-dir.stderr.ndjson | 0 .../fixtures/mcp-second-add-dir.stdin.ndjson | 1 - .../fixtures/mcp-second-add-dir.stdout.ndjson | 71 -- .../model-switch-invalid.capture.ndjson | 3 - .../model-switch-invalid.stderr.ndjson | 13 - .../model-switch-invalid.stdin.ndjson | 0 .../model-switch-invalid.stdout.ndjson | 1 - .../model-switch-network.capture.ndjson | 3 - .../model-switch-network.stderr.ndjson | 0 .../model-switch-network.stdin.ndjson | 1 - .../model-switch-network.stdout.ndjson | 8 - .../model-switch-resume.capture.ndjson | 3 - .../model-switch-resume.stderr.ndjson | 13 - .../fixtures/model-switch-resume.stdin.ndjson | 0 .../model-switch-resume.stdout.ndjson | 1 - .../model-switch-valid.capture.ndjson | 2 - .../fixtures/model-switch-valid.stderr.ndjson | 0 .../fixtures/model-switch-valid.stdin.ndjson | 1 - .../fixtures/model-switch-valid.stdout.ndjson | 7 - .../fixtures/models-api-key.capture.ndjson | 2 - .../fixtures/models-api-key.stderr.ndjson | 1 - .../test/fixtures/models-api-key.stdin.ndjson | 0 .../fixtures/models-api-key.stdout.ndjson | 11 - .../models-unauthenticated.capture.ndjson | 2 - .../models-unauthenticated.stderr.ndjson | 2 - .../models-unauthenticated.stdin.ndjson | 0 .../models-unauthenticated.stdout.ndjson | 0 .../multi-turn-reasoning.capture.ndjson | 2 - .../multi-turn-reasoning.stderr.ndjson | 0 .../multi-turn-reasoning.stdin.ndjson | 2 - .../multi-turn-reasoning.stdout.ndjson | 51 -- .../fixtures/not-signed-in.capture.ndjson | 2 - .../test/fixtures/not-signed-in.stderr.ndjson | 2 - .../test/fixtures/not-signed-in.stdin.ndjson | 0 .../test/fixtures/not-signed-in.stdout.ndjson | 1 - .../test/fixtures/parity-inspection.ndjson | 10 - .../fixtures/project-skill.capture.ndjson | 2 - .../test/fixtures/project-skill.stderr.ndjson | 0 .../test/fixtures/project-skill.stdin.ndjson | 1 - .../test/fixtures/project-skill.stdout.ndjson | 9 - .../question-auto-denial.capture.ndjson | 2 - .../question-auto-denial.stderr.ndjson | 0 .../question-auto-denial.stdin.ndjson | 1 - .../question-auto-denial.stdout.ndjson | 14 - .../resume-after-interrupt.capture.ndjson | 2 - .../resume-after-interrupt.stderr.ndjson | 0 .../resume-after-interrupt.stdin.ndjson | 1 - .../resume-after-interrupt.stdout.ndjson | 7 - .../test/fixtures/resume.capture.ndjson | 2 - .../test/fixtures/resume.stderr.ndjson | 0 .../test/fixtures/resume.stdin.ndjson | 1 - .../test/fixtures/resume.stdout.ndjson | 8 - .../fixtures/schema-prompt.capture.ndjson | 3 - .../test/fixtures/schema-prompt.stderr.ndjson | 1 - .../test/fixtures/schema-prompt.stdin.ndjson | 1 - .../test/fixtures/schema-prompt.stdout.ndjson | 1 - .../schema-user-message.capture.ndjson | 2 - .../schema-user-message.stderr.ndjson | 1 - .../fixtures/schema-user-message.stdin.ndjson | 1 - .../schema-user-message.stdout.ndjson | 2 - .../test/fixtures/schema-user.capture.ndjson | 2 - .../test/fixtures/schema-user.stderr.ndjson | 1 - .../test/fixtures/schema-user.stdin.ndjson | 1 - .../test/fixtures/schema-user.stdout.ndjson | 2 - .../fixtures/sigint-mid-text.capture.ndjson | 3 - .../fixtures/sigint-mid-text.stderr.ndjson | 1 - .../fixtures/sigint-mid-text.stdin.ndjson | 1 - .../fixtures/sigint-mid-text.stdout.ndjson | 4 - .../fixtures/sigint-mid-tool.capture.ndjson | 3 - .../fixtures/sigint-mid-tool.stderr.ndjson | 1 - .../fixtures/sigint-mid-tool.stdin.ndjson | 1 - .../fixtures/sigint-mid-tool.stdout.ndjson | 5 - .../test/fixtures/sigterm.capture.ndjson | 3 - .../test/fixtures/sigterm.stderr.ndjson | 1 - .../test/fixtures/sigterm.stdin.ndjson | 1 - .../test/fixtures/sigterm.stdout.ndjson | 3 - .../fixtures/slash-disabled.capture.ndjson | 2 - .../fixtures/slash-disabled.stderr.ndjson | 0 .../test/fixtures/slash-disabled.stdin.ndjson | 1 - .../fixtures/slash-disabled.stdout.ndjson | 84 -- .../fixtures/slash-enabled.capture.ndjson | 2 - .../test/fixtures/slash-enabled.stderr.ndjson | 1 - .../test/fixtures/slash-enabled.stdin.ndjson | 1 - .../test/fixtures/slash-enabled.stdout.ndjson | 2 - .../test/fixtures/state-inspection.ndjson | 6 - .../stdin-close-mid-turn.capture.ndjson | 3 - .../stdin-close-mid-turn.stderr.ndjson | 0 .../stdin-close-mid-turn.stdin.ndjson | 1 - .../stdin-close-mid-turn.stdout.ndjson | 5 - .../fixtures/stdin-mid-turn.capture.ndjson | 2 - .../fixtures/stdin-mid-turn.stderr.ndjson | 0 .../test/fixtures/stdin-mid-turn.stdin.ndjson | 2 - .../fixtures/stdin-mid-turn.stdout.ndjson | 179 ----- .../test/fixtures/subagent.capture.ndjson | 2 - .../test/fixtures/subagent.stderr.ndjson | 0 .../test/fixtures/subagent.stdin.ndjson | 1 - .../test/fixtures/subagent.stdout.ndjson | 23 - .../test/fixtures/text-block.capture.ndjson | 2 - .../test/fixtures/text-block.stderr.ndjson | 0 .../test/fixtures/text-block.stdin.ndjson | 1 - .../test/fixtures/text-block.stdout.ndjson | 5 - .../test/fixtures/text-turn.capture.ndjson | 2 - .../test/fixtures/text-turn.stderr.ndjson | 0 .../test/fixtures/text-turn.stdin.ndjson | 1 - .../test/fixtures/text-turn.stdout.ndjson | 5 - .../tools-accept-edits.capture.ndjson | 2 - .../fixtures/tools-accept-edits.stderr.ndjson | 1 - .../fixtures/tools-accept-edits.stdin.ndjson | 3 - .../fixtures/tools-accept-edits.stdout.ndjson | 40 - .../fixtures/tools-default.capture.ndjson | 2 - .../test/fixtures/tools-default.stderr.ndjson | 1 - .../test/fixtures/tools-default.stdin.ndjson | 3 - .../test/fixtures/tools-default.stdout.ndjson | 41 - .../test/fixtures/tools-plan.capture.ndjson | 2 - .../test/fixtures/tools-plan.stderr.ndjson | 4 - .../test/fixtures/tools-plan.stdin.ndjson | 3 - .../test/fixtures/tools-plan.stdout.ndjson | 36 - .../fixtures/tools-sandbox.capture.ndjson | 2 - .../test/fixtures/tools-sandbox.stderr.ndjson | 1 - .../test/fixtures/tools-sandbox.stdin.ndjson | 3 - .../test/fixtures/tools-sandbox.stdout.ndjson | 40 - .../tools-skip-permissions.capture.ndjson | 2 - .../tools-skip-permissions.stderr.ndjson | 0 .../tools-skip-permissions.stdin.ndjson | 3 - .../tools-skip-permissions.stdout.ndjson | 42 - .../test/fixtures/version.capture.ndjson | 2 - .../test/fixtures/version.stderr.ndjson | 0 .../test/fixtures/version.stdin.ndjson | 0 .../test/fixtures/version.stdout.ndjson | 1 - public-docs/providers.md | 4 +- public-docs/supported-providers.md | 31 - 266 files changed, 127 insertions(+), 3729 deletions(-) delete mode 100644 packages/website/src/routes/antigravity.tsx delete mode 100644 plugins/antigravity-provider/icon.svg delete mode 100644 plugins/antigravity-provider/index.server.ts delete mode 100644 plugins/antigravity-provider/paseo-plugin.json delete mode 100644 plugins/antigravity-provider/server/internal/catalog.ts delete mode 100644 plugins/antigravity-provider/server/internal/process.ts delete mode 100644 plugins/antigravity-provider/server/internal/prompt.ts delete mode 100644 plugins/antigravity-provider/server/internal/session.ts delete mode 100644 plugins/antigravity-provider/server/internal/signals.ts delete mode 100644 plugins/antigravity-provider/server/internal/timeline.ts delete mode 100644 plugins/antigravity-provider/server/internal/wire.ts delete mode 100644 plugins/antigravity-provider/server/provider.test.ts delete mode 100644 plugins/antigravity-provider/server/provider.ts delete mode 100644 plugins/antigravity-provider/test/agy.mjs delete mode 100644 plugins/antigravity-provider/test/fixtures/README.md delete mode 100644 plugins/antigravity-provider/test/fixtures/agents.capture.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/agents.stderr.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/agents.stdin.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/agents.stdout.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/api-key-env-only.capture.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/api-key-env-only.stderr.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/api-key-env-only.stdin.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/api-key-env-only.stdout.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/api-key-settings.capture.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/api-key-settings.stderr.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/api-key-settings.stdin.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/api-key-settings.stdout.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/cwd-without-add-dir.capture.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/cwd-without-add-dir.stderr.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/cwd-without-add-dir.stdin.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/cwd-without-add-dir.stdout.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/denial-replay.stdout.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/effort-encoded-match.capture.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/effort-encoded-match.stderr.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/effort-encoded-match.stdin.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/effort-encoded-match.stdout.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/effort-encoded-mismatch.capture.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/effort-encoded-mismatch.stderr.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/effort-encoded-mismatch.stdin.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/effort-encoded-mismatch.stdout.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/effort-mismatch.capture.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/effort-mismatch.stderr.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/effort-mismatch.stdin.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/effort-mismatch.stdout.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/effort-unsupported-family.capture.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/effort-unsupported-family.stderr.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/effort-unsupported-family.stdin.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/effort-unsupported-family.stdout.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/help.capture.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/help.stderr.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/help.stdin.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/help.stdout.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/image-content.capture.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/image-content.stderr.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/image-content.stdin.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/image-content.stdout.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/image-file-external.capture.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/image-file-external.png delete mode 100644 plugins/antigravity-provider/test/fixtures/image-file-external.stderr.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/image-file-external.stdin.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/image-file-external.stdout.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/mcp-add-http.capture.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/mcp-add-http.stderr.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/mcp-add-http.stdin.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/mcp-add-http.stdout.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/mcp-add.capture.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/mcp-add.stderr.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/mcp-add.stdin.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/mcp-add.stdout.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/mcp-agent-definition.capture.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/mcp-agent-definition.stderr.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/mcp-agent-definition.stdin.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/mcp-agent-definition.stdout.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/mcp-agent-discovered.capture.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/mcp-agent-discovered.stderr.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/mcp-agent-discovered.stdin.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/mcp-agent-discovered.stdout.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/mcp-agent-file.capture.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/mcp-agent-file.stderr.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/mcp-agent-file.stdin.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/mcp-agent-file.stdout.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/mcp-global.capture.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/mcp-global.stderr.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/mcp-global.stdin.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/mcp-global.stdout.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/mcp-help.capture.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/mcp-help.stderr.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/mcp-help.stdin.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/mcp-help.stdout.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/mcp-http.requests.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/mcp-list.capture.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/mcp-list.stderr.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/mcp-list.stdin.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/mcp-list.stdout.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/mcp-owned-cwd.capture.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/mcp-owned-cwd.stderr.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/mcp-owned-cwd.stdin.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/mcp-owned-cwd.stdout.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/mcp-owned-first-add-dir.capture.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/mcp-owned-first-add-dir.stderr.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/mcp-owned-first-add-dir.stdin.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/mcp-owned-first-add-dir.stdout.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/mcp-project-config.capture.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/mcp-project-config.stderr.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/mcp-project-config.stdin.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/mcp-project-config.stdout.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/mcp-project-isolated.capture.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/mcp-project-isolated.stderr.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/mcp-project-isolated.stdin.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/mcp-project-isolated.stdout.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/mcp-project.capture.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/mcp-project.stderr.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/mcp-project.stdin.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/mcp-project.stdout.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/mcp-second-add-dir.capture.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/mcp-second-add-dir.stderr.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/mcp-second-add-dir.stdin.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/mcp-second-add-dir.stdout.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/model-switch-invalid.capture.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/model-switch-invalid.stderr.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/model-switch-invalid.stdin.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/model-switch-invalid.stdout.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/model-switch-network.capture.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/model-switch-network.stderr.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/model-switch-network.stdin.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/model-switch-network.stdout.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/model-switch-resume.capture.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/model-switch-resume.stderr.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/model-switch-resume.stdin.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/model-switch-resume.stdout.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/model-switch-valid.capture.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/model-switch-valid.stderr.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/model-switch-valid.stdin.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/model-switch-valid.stdout.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/models-api-key.capture.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/models-api-key.stderr.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/models-api-key.stdin.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/models-api-key.stdout.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/models-unauthenticated.capture.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/models-unauthenticated.stderr.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/models-unauthenticated.stdin.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/models-unauthenticated.stdout.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/multi-turn-reasoning.capture.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/multi-turn-reasoning.stderr.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/multi-turn-reasoning.stdin.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/multi-turn-reasoning.stdout.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/not-signed-in.capture.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/not-signed-in.stderr.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/not-signed-in.stdin.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/not-signed-in.stdout.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/parity-inspection.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/project-skill.capture.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/project-skill.stderr.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/project-skill.stdin.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/project-skill.stdout.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/question-auto-denial.capture.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/question-auto-denial.stderr.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/question-auto-denial.stdin.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/question-auto-denial.stdout.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/resume-after-interrupt.capture.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/resume-after-interrupt.stderr.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/resume-after-interrupt.stdin.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/resume-after-interrupt.stdout.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/resume.capture.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/resume.stderr.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/resume.stdin.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/resume.stdout.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/schema-prompt.capture.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/schema-prompt.stderr.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/schema-prompt.stdin.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/schema-prompt.stdout.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/schema-user-message.capture.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/schema-user-message.stderr.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/schema-user-message.stdin.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/schema-user-message.stdout.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/schema-user.capture.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/schema-user.stderr.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/schema-user.stdin.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/schema-user.stdout.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/sigint-mid-text.capture.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/sigint-mid-text.stderr.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/sigint-mid-text.stdin.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/sigint-mid-text.stdout.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/sigint-mid-tool.capture.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/sigint-mid-tool.stderr.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/sigint-mid-tool.stdin.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/sigint-mid-tool.stdout.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/sigterm.capture.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/sigterm.stderr.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/sigterm.stdin.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/sigterm.stdout.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/slash-disabled.capture.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/slash-disabled.stderr.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/slash-disabled.stdin.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/slash-disabled.stdout.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/slash-enabled.capture.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/slash-enabled.stderr.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/slash-enabled.stdin.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/slash-enabled.stdout.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/state-inspection.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/stdin-close-mid-turn.capture.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/stdin-close-mid-turn.stderr.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/stdin-close-mid-turn.stdin.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/stdin-close-mid-turn.stdout.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/stdin-mid-turn.capture.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/stdin-mid-turn.stderr.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/stdin-mid-turn.stdin.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/stdin-mid-turn.stdout.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/subagent.capture.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/subagent.stderr.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/subagent.stdin.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/subagent.stdout.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/text-block.capture.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/text-block.stderr.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/text-block.stdin.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/text-block.stdout.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/text-turn.capture.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/text-turn.stderr.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/text-turn.stdin.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/text-turn.stdout.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/tools-accept-edits.capture.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/tools-accept-edits.stderr.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/tools-accept-edits.stdin.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/tools-accept-edits.stdout.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/tools-default.capture.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/tools-default.stderr.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/tools-default.stdin.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/tools-default.stdout.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/tools-plan.capture.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/tools-plan.stderr.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/tools-plan.stdin.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/tools-plan.stdout.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/tools-sandbox.capture.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/tools-sandbox.stderr.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/tools-sandbox.stdin.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/tools-sandbox.stdout.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/tools-skip-permissions.capture.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/tools-skip-permissions.stderr.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/tools-skip-permissions.stdin.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/tools-skip-permissions.stdout.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/version.capture.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/version.stderr.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/version.stdin.ndjson delete mode 100644 plugins/antigravity-provider/test/fixtures/version.stdout.ndjson diff --git a/CLAUDE.md b/CLAUDE.md index e1ac653338..80aabf3910 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,7 +2,7 @@ Paseo is a mobile app for monitoring and controlling your local AI coding agents from anywhere. Your dev environment, in your pocket. Connects directly to your actual development environment — your code stays on your machine. -**Supported agents:** Claude Code, Codex, GitHub Copilot, OpenCode, Pi, Antigravity, and Muse Code. +**Supported agents:** Claude Code, Codex, GitHub Copilot, OpenCode, Pi, and Muse Code. ## Repository map diff --git a/README.md b/README.md index aa0a841e7e..629477a3ad 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@

-

One interface for Claude Code, Codex, Copilot, OpenCode, Pi, Antigravity, and Muse Code agents.

+

One interface for Claude Code, Codex, Copilot, OpenCode, Pi, and Muse Code agents.

Paseo app screenshot @@ -42,7 +42,7 @@ Run agents in parallel on your own machines. Ship from your phone or your desk. - **Self-hosted:** Agents run on your machine with your full dev environment. Use your tools, your configs, and your skills. -- **Multi-provider:** Claude Code, Codex, Copilot, OpenCode, Pi, Antigravity, and Muse Code through the same interface. Pick the right model for each job. +- **Multi-provider:** Claude Code, Codex, Copilot, OpenCode, Pi, and Muse Code through the same interface. Pick the right model for each job. - **Voice control:** Dictate tasks or talk through problems in voice mode. Hands-free when you need it. - **Cross-device:** iOS, Android, desktop, web, and CLI. Start work at your desk, check in from your phone, script it from the terminal. - **Privacy-first:** Paseo doesn't have any telemetry, tracking, or forced log-ins. @@ -68,7 +68,6 @@ You need at least one agent CLI installed and configured with your credentials: - [GitHub Copilot](https://github.com/features/copilot/cli/) - [OpenCode](https://github.com/anomalyco/opencode) - [Pi](https://pi.dev) -- [Antigravity](https://paseo.sh/docs/supported-providers#antigravity) - [Muse Code](https://paseo.sh/docs/muse-code) ### Desktop app (recommended) diff --git a/docs/providers.md b/docs/providers.md index 0036c2e6f2..0558406a1b 100644 --- a/docs/providers.md +++ b/docs/providers.md @@ -6,7 +6,7 @@ server integrations. ## Plugin providers Keep a bundled provider in `plugins//` and register it through -`@getpaseo/plugin/server/provider`. Antigravity and Muse Code follow this pattern. Built-in loading and SDK +`@getpaseo/plugin/server/provider`. Muse Code follows this pattern. Built-in loading and SDK import rules belong to [plugins.md](plugins.md#built-in-plugins); the [public provider guide](../public-docs/plugins/providers.md) covers the provider contract. @@ -15,10 +15,9 @@ executable resolution and applies `agents.providers..command` and ` connecting. Register the provider's icon with the plugin rather than adding it to the app's provider icon map. You do not need a core manifest entry or provider factory. -| Provider | Transport | Setup and limitations | -| ----------- | ------------------------------------------ | ---------------------------------------------------------------- | -| Antigravity | Installed `agy` CLI | [Antigravity](../public-docs/supported-providers.md#antigravity) | -| Muse Code | MSP over one `muse serve` host per session | [Muse Code](../public-docs/muse-code.md) | +| Provider | Transport | Setup and limitations | +| --------- | ------------------------------------------ | ---------------------------------------- | +| Muse Code | MSP over one `muse serve` host per session | [Muse Code](../public-docs/muse-code.md) | ## Provider-native session options diff --git a/packages/server/src/server/daemon-e2e/models.e2e.test.ts b/packages/server/src/server/daemon-e2e/models.e2e.test.ts index ff93b695c1..7691e63713 100644 --- a/packages/server/src/server/daemon-e2e/models.e2e.test.ts +++ b/packages/server/src/server/daemon-e2e/models.e2e.test.ts @@ -1,11 +1,4 @@ import { describe, test, expect } from "vitest"; -import { mkdtemp, rm } from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import { findExecutable } from "../../executable-resolution/executable-resolution.js"; -import { BuiltinPluginLoader } from "../plugins/builtin/index.js"; -import { createTestPaseoDaemon } from "../test-utils/paseo-daemon.js"; -import { DaemonClient } from "../test-utils/daemon-client.js"; import { execFileSync } from "node:child_process"; import { createDaemonTestContext } from "../test-utils/index.js"; @@ -103,54 +96,3 @@ describe("daemon E2E", () => { ); }); }); - -test("Antigravity creates an agent and answers a prompt with real agy", async ({ skip }) => { - const command = await findExecutable(process.env.AGY_COMMAND ?? "agy"); - if (command === null) { - skip("agy is not resolvable"); - return; - } - const cwd = await mkdtemp(path.join(os.tmpdir(), "paseo-antigravity-e2e-")); - const daemon = await createTestPaseoDaemon({ - agentClients: {}, - mcpEnabled: false, - builtinPlugins: new BuiltinPluginLoader(undefined, ["antigravity-provider"]), - providerOverrides: { antigravity: { command: [command], paseoTools: { enabled: false } } }, - }); - const client = new DaemonClient({ - url: `ws://127.0.0.1:${daemon.port}/ws`, - appVersion: "0.10.0", - }); - try { - await client.connect(); - await client.fetchAgents({ subscribe: {} }); - await expect - .poll( - async () => { - const snapshot = await client.getProvidersSnapshot({ cwd }); - return snapshot.entries.find((entry) => entry.provider === "antigravity")?.status; - }, - { timeout: 30000 }, - ) - .toBe("ready"); - const agent = await client.createAgent({ - provider: "antigravity", - cwd, - title: "Antigravity E2E", - }); - await client.sendMessage(agent.id, "Reply with exactly ANTIGRAVITY_E2E_OK. No tools."); - const result = await client.waitForFinish(agent.id, 90000); - expect(result.status).toBe("idle"); - const timeline = await client.fetchAgentTimeline(agent.id, { limit: 100 }); - expect( - timeline.entries.some( - (entry) => - entry.item.type === "assistant_message" && entry.item.text.includes("ANTIGRAVITY_E2E_OK"), - ), - ).toBe(true); - } finally { - await client.close(); - await daemon.close(); - await rm(cwd, { recursive: true, force: true }); - } -}, 120000); diff --git a/packages/server/src/server/plugins/builtin/index.ts b/packages/server/src/server/plugins/builtin/index.ts index eb6aab147e..179768dd2f 100644 --- a/packages/server/src/server/plugins/builtin/index.ts +++ b/packages/server/src/server/plugins/builtin/index.ts @@ -3,7 +3,6 @@ import path from "node:path"; import { fileURLToPath } from "node:url"; export const builtinPlugins = [ - "antigravity-provider", "claude-usage-source", "codex-usage-source", "copilot-usage-source", diff --git a/packages/website/src/components/agent-icons.tsx b/packages/website/src/components/agent-icons.tsx index 8488050b12..e827d1e484 100644 --- a/packages/website/src/components/agent-icons.tsx +++ b/packages/website/src/components/agent-icons.tsx @@ -99,20 +99,3 @@ export function MuseCodeIcon(props: React.SVGProps) { ); } - -export function AntigravityIcon(props: React.SVGProps) { - return ( - - ); -} diff --git a/packages/website/src/components/landing-page.tsx b/packages/website/src/components/landing-page.tsx index 6a29c62235..db25dbe1ea 100644 --- a/packages/website/src/components/landing-page.tsx +++ b/packages/website/src/components/landing-page.tsx @@ -79,7 +79,6 @@ import { OpenCodeIcon, PiIcon, MuseCodeIcon, - AntigravityIcon, } from "~/components/agent-icons"; import { DiscordIcon, GitHubIcon, SlackIcon } from "~/components/brand-icons"; import { ClaudeIcon, MobileChat, MobileDiff, MobileSidebar, PhoneFrame } from "~/components/mockup"; @@ -163,7 +162,6 @@ function Hero({ title, subtitle }: { title: React.ReactNode; subtitle: React.Rea const CLAUDE_CODE_BADGE_ICON = ; const CODEX_BADGE_ICON = ; -const ANTIGRAVITY_BADGE_ICON = ; const MUSE_CODE_BADGE_ICON = ; const OPENCODE_BADGE_ICON = ; const PI_BADGE_ICON = ; @@ -436,7 +434,6 @@ function MultiProviderSection() { { name: "Claude Code", icon: }, { name: "Codex", icon: }, { name: "OpenCode", icon: }, - { name: "Antigravity", icon: }, { name: "Muse Code", icon: }, { name: "Pi", icon: }, { name: "Cursor", icon: }, @@ -906,8 +903,7 @@ const PRINCIPLES: ReadonlyArray<{ icon: LucideIcon; title: string; description: { icon: Merge, title: "Unified", - description: - "Claude Code, Codex, OpenCode, Pi, Antigravity, Muse Code, and more, all used the same way.", + description: "Claude Code, Codex, OpenCode, Pi, Muse Code, and more, all used the same way.", }, { icon: Blocks, @@ -969,7 +965,6 @@ function GetStarted() { - @@ -1216,8 +1211,7 @@ function FAQ() { Paseo supports many providers. It has custom implementations for Claude, Codex, OpenCode, - Pi, OMP, Antigravity, and Muse Code, and supports many more via ACP. See the full list - here:{" "} + Pi, OMP, and Muse Code, and supports many more via ACP. See the full list here:{" "} all supported providers diff --git a/packages/website/src/components/site-footer.tsx b/packages/website/src/components/site-footer.tsx index ba092410da..9009e63e53 100644 --- a/packages/website/src/components/site-footer.tsx +++ b/packages/website/src/components/site-footer.tsx @@ -86,12 +86,6 @@ export function SiteFooter({ width = "default" }: SiteFooterProps) { > OpenCode - - Antigravity - rootRouteImport, } as any); -const AntigravityRoute = AntigravityRouteImport.update({ - id: "/antigravity", - path: "/antigravity", - getParentRoute: () => rootRouteImport, -} as any); const AmpRoute = AmpRouteImport.update({ id: "/amp", path: "/amp", @@ -396,7 +390,6 @@ export interface FileRoutesByFullPath { "/agents": typeof AgentsRoute; "/agoragentic": typeof AgoragenticRoute; "/amp": typeof AmpRoute; - "/antigravity": typeof AntigravityRoute; "/auggie": typeof AuggieRoute; "/autohand": typeof AutohandRoute; "/blog": typeof BlogRouteWithChildren; @@ -461,7 +454,6 @@ export interface FileRoutesByTo { "/agents": typeof AgentsRoute; "/agoragentic": typeof AgoragenticRoute; "/amp": typeof AmpRoute; - "/antigravity": typeof AntigravityRoute; "/auggie": typeof AuggieRoute; "/autohand": typeof AutohandRoute; "/changelog": typeof ChangelogRoute; @@ -525,7 +517,6 @@ export interface FileRoutesById { "/agents": typeof AgentsRoute; "/agoragentic": typeof AgoragenticRoute; "/amp": typeof AmpRoute; - "/antigravity": typeof AntigravityRoute; "/auggie": typeof AuggieRoute; "/autohand": typeof AutohandRoute; "/blog": typeof BlogRouteWithChildren; @@ -592,7 +583,6 @@ export interface FileRouteTypes { | "/agents" | "/agoragentic" | "/amp" - | "/antigravity" | "/auggie" | "/autohand" | "/blog" @@ -657,7 +647,6 @@ export interface FileRouteTypes { | "/agents" | "/agoragentic" | "/amp" - | "/antigravity" | "/auggie" | "/autohand" | "/changelog" @@ -720,7 +709,6 @@ export interface FileRouteTypes { | "/agents" | "/agoragentic" | "/amp" - | "/antigravity" | "/auggie" | "/autohand" | "/blog" @@ -786,7 +774,6 @@ export interface RootRouteChildren { AgentsRoute: typeof AgentsRoute; AgoragenticRoute: typeof AgoragenticRoute; AmpRoute: typeof AmpRoute; - AntigravityRoute: typeof AntigravityRoute; AuggieRoute: typeof AuggieRoute; AutohandRoute: typeof AutohandRoute; BlogRoute: typeof BlogRouteWithChildren; @@ -1167,13 +1154,6 @@ declare module "@tanstack/react-router" { preLoaderRoute: typeof AuggieRouteImport; parentRoute: typeof rootRouteImport; }; - "/antigravity": { - id: "/antigravity"; - path: "/antigravity"; - fullPath: "/antigravity"; - preLoaderRoute: typeof AntigravityRouteImport; - parentRoute: typeof rootRouteImport; - }; "/amp": { id: "/amp"; path: "/amp"; @@ -1318,7 +1298,6 @@ const rootRouteChildren: RootRouteChildren = { AgentsRoute: AgentsRoute, AgoragenticRoute: AgoragenticRoute, AmpRoute: AmpRoute, - AntigravityRoute: AntigravityRoute, AuggieRoute: AuggieRoute, AutohandRoute: AutohandRoute, BlogRoute: BlogRouteWithChildren, diff --git a/packages/website/src/routes/agents.tsx b/packages/website/src/routes/agents.tsx index 03548fe3bf..551eddc94b 100644 --- a/packages/website/src/routes/agents.tsx +++ b/packages/website/src/routes/agents.tsx @@ -10,7 +10,7 @@ export const Route = createFileRoute("/agents")({ head: () => pageMeta( "Supported agents – Every coding agent Paseo runs", - "Run Claude Code, Codex, Copilot, OpenCode, Antigravity, Muse Code, Cursor CLI, Gemini CLI, and dozens more coding agents from your phone. Self-hosted, your code stays on your machine.", + "Run Claude Code, Codex, Copilot, OpenCode, Muse Code, Cursor CLI, Gemini CLI, and dozens more coding agents from your phone. Self-hosted, your code stays on your machine.", "/agents", ), component: AgentsPage, diff --git a/packages/website/src/routes/antigravity.tsx b/packages/website/src/routes/antigravity.tsx deleted file mode 100644 index 3d821de7b9..0000000000 --- a/packages/website/src/routes/antigravity.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createFileRoute } from "@tanstack/react-router"; -import { agentRouteOptions } from "~/components/agent-route"; - -export const Route = createFileRoute("/antigravity")(agentRouteOptions("antigravity")); diff --git a/plugins/antigravity-provider/icon.svg b/plugins/antigravity-provider/icon.svg deleted file mode 100644 index 0576d92a87..0000000000 --- a/plugins/antigravity-provider/icon.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/plugins/antigravity-provider/index.server.ts b/plugins/antigravity-provider/index.server.ts deleted file mode 100644 index 661fa8445d..0000000000 --- a/plugins/antigravity-provider/index.server.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { PluginServerContext } from "@getpaseo/plugin/server"; -import { createAntigravityProvider } from "./server/provider.js"; - -export default function contribute(server: PluginServerContext) { - server.registerProvider(createAntigravityProvider()); - return () => {}; -} diff --git a/plugins/antigravity-provider/paseo-plugin.json b/plugins/antigravity-provider/paseo-plugin.json deleted file mode 100644 index ce6b2834f2..0000000000 --- a/plugins/antigravity-provider/paseo-plugin.json +++ /dev/null @@ -1 +0,0 @@ -{ "id": "antigravity-provider", "requirements": { "paseo": ">=0.10.0" } } diff --git a/plugins/antigravity-provider/server/internal/catalog.ts b/plugins/antigravity-provider/server/internal/catalog.ts deleted file mode 100644 index 74e3be583e..0000000000 --- a/plugins/antigravity-provider/server/internal/catalog.ts +++ /dev/null @@ -1,96 +0,0 @@ -import type { - ProviderCatalog, - ProviderLaunch, - ProviderModel, - ProviderMode, - ProviderStatus, -} from "@getpaseo/plugin/server/provider"; -import { probe } from "./process.js"; -import { AntigravityError } from "./wire.js"; - -const modes: readonly ProviderMode[] = [ - { - id: "default", - icon: "Shield", - colorTier: "moderate", - label: "Default", - description: "Workspace reads and edits are allowed; shell commands are denied.", - }, - { - id: "accept-edits", - icon: "ShieldPlus", - colorTier: "moderate", - label: "Accept edits", - description: - "Native accept-edits mode. Workspace edits are allowed; shell commands are denied.", - }, - { - id: "plan", - icon: "ShieldEllipsis", - colorTier: "planning", - label: "Plan", - description: - "Native plan mode permits workspace edits. Shell commands are denied; this is not read-only.", - }, - { - id: "full-access", - icon: "ShieldOff", - colorTier: "dangerous", - label: "Full access", - description: "Skip native permission checks, including for shell commands.", - isUnattended: true, - }, -]; - -export async function getCatalog(launch: ProviderLaunch, cwd?: string): Promise { - const output = await probe({ launch, args: ["models"], cwd }); - const models: ProviderModel[] = []; - for (const line of output.trim().split(/\r?\n/)) { - const [id, label, ...extra] = line.split("\t"); - if (!id || !label || extra.length > 0) - throw new AntigravityError("Invalid `agy models` output", "INVALID_CATALOG"); - if (models.some((model) => model.id === id)) - throw new AntigravityError(`Duplicate Antigravity model: ${id}`, "INVALID_CATALOG"); - models.push({ id, label }); - } - return { models, modes, thinkingOptions: [], defaultMode: "default" }; -} - -export async function getStatus(launch: ProviderLaunch): Promise { - try { - const version = (await probe({ launch, args: ["--version"] })).trim(); - const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(version); - if (!match) - return { - available: false, - diagnostic: "Could not read the Antigravity version. Update `agy` to 1.1.15 or later.", - }; - const [, major, minor, patch] = match.map(Number); - const supported = major > 1 || (major === 1 && (minor > 1 || (minor === 1 && patch >= 15))); - if (!supported) - return { - available: false, - diagnostic: `Antigravity ${version} is unsupported. Update \`agy\` to 1.1.15 or later.`, - }; - await getCatalog(launch); - return { available: true }; - } catch (error) { - if (!(error instanceof AntigravityError)) throw error; - return { available: false, diagnostic: error.message }; - } -} - -export function validateSelection( - config: { model?: string; mode?: string; thinkingOption?: string }, - catalog: ProviderCatalog, -): void { - if (config.model && !catalog.models.some((model) => model.id === config.model)) - throw new AntigravityError(`Unknown Antigravity model: ${config.model}`, "INVALID_MODEL"); - if (config.mode && !catalog.modes.some((mode) => mode.id === config.mode)) - throw new AntigravityError(`Unknown Antigravity mode: ${config.mode}`, "INVALID_MODE"); - if (config.thinkingOption) - throw new AntigravityError( - "Antigravity effort is selected through the model ID", - "UNSUPPORTED_THINKING", - ); -} diff --git a/plugins/antigravity-provider/server/internal/process.ts b/plugins/antigravity-provider/server/internal/process.ts deleted file mode 100644 index a1b2f1c7b8..0000000000 --- a/plugins/antigravity-provider/server/internal/process.ts +++ /dev/null @@ -1,224 +0,0 @@ -import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process"; -import { signalProcess } from "./signals.js"; -import { createInterface } from "node:readline"; -import type { ProviderLaunch, ProviderSessionConfig } from "@getpaseo/plugin/server/provider"; -import { - AntigravityError, - decodeFrame, - diagnostic, - encodePrompt, - type Frame, - type Init, -} from "./wire.js"; - -interface DriverOptions { - launch: ProviderLaunch; - config: ProviderSessionConfig; - conversationId: string | null; - onFrame(frame: Frame): void; - onExit(error: AntigravityError): void; -} - -export interface Driver { - ready: Promise; - selection: string; - prompt(text: string): Promise; - stop(reason: "interrupt" | "close"): Promise; -} - -export function selection(config: ProviderSessionConfig): string { - return JSON.stringify([config.model, config.mode]); -} - -function driverArgs(options: DriverOptions): string[] { - const { config, conversationId } = options; - const args = [ - ...options.launch.args, - "--input-format", - "stream-json", - "--output-format", - "stream-json", - "--add-dir", - config.cwd, - "--disable-slash-commands", - ]; - if (config.model) args.push("--model", config.model); - if (config.mode === "accept-edits" || config.mode === "plan") args.push("--mode", config.mode); - if (config.mode === "full-access") args.push("--dangerously-skip-permissions"); - if (conversationId) args.push("--conversation", conversationId); - return args; -} - -export function startDriver(options: DriverOptions): Driver { - const child = spawn(options.launch.command, driverArgs(options), { - cwd: options.config.cwd, - env: { ...options.launch.env, ...options.config.env }, - detached: process.platform !== "win32", - stdio: "pipe", - }); - const lines = createInterface({ input: child.stdout }); - let cleanup = Promise.resolve(); - let stderr = ""; - let state: "starting" | "ready" | "stopping" | "exited" = "starting"; - let resolveReady: (init: Init) => void; - let rejectReady: (error: Error) => void; - const ready = new Promise((resolve, reject) => { - resolveReady = resolve; - rejectReady = reject; - }); - let resolveExit: () => void; - const exited = new Promise((resolve) => { - resolveExit = resolve; - }); - const startupDeadline = setTimeout( - () => - fail( - new AntigravityError("Antigravity did not initialize within 30 seconds", "STARTUP_TIMEOUT"), - ), - 30_000, - ); - child.stderr.on("data", (chunk: Buffer) => { - stderr = (stderr + chunk.toString()).slice(-8192); - }); - child.stdin.on("error", (error: Error) => - fail(new AntigravityError(error.message, "STDIN_ERROR")), - ); - child.on("error", (error: Error) => fail(new AntigravityError(error.message, "SPAWN_ERROR"))); - child.on("close", (code, signal) => { - clearTimeout(startupDeadline); - lines.close(); - const error = new AntigravityError( - diagnostic(stderr.trim() || `Antigravity exited (${signal || code})`), - "PROCESS_EXIT", - ); - if (state === "starting") rejectReady(error); - if (state === "ready") options.onExit(error); - state = "exited"; - resolveExit(); - }); - lines.on("line", (line) => { - if (!line.trim() || state === "exited" || state === "stopping") return; - let decoded: Frame; - try { - decoded = decodeFrame(line); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - fail(new AntigravityError(`Invalid Antigravity output: ${message}`, "INVALID_FRAME")); - return; - } - if (state === "starting") { - if (decoded.event === "init") { - clearTimeout(startupDeadline); - state = "ready"; - resolveReady(decoded); - } else if (decoded.event === "result") { - fail(new AntigravityError(diagnostic(decoded.result.error), "STARTUP_ERROR")); - } - return; - } - options.onFrame(decoded); - }); - - function fail(failure: AntigravityError): void { - clearTimeout(startupDeadline); - if (state === "starting") rejectReady(failure); - if (state === "ready") options.onExit(failure); - if (state === "exited" || state === "stopping") return; - state = "stopping"; - cleanup = Promise.resolve(signalGroup(child, "SIGKILL")); - // The stop operation awaits cleanup and owns reporting its failure. - void cleanup.catch(() => undefined); - } - - return { - ready, - selection: selection(options.config), - prompt(text) { - if (state !== "ready") - return Promise.reject(new AntigravityError("Antigravity is not ready")); - return new Promise((resolve, reject) => { - child.stdin.write(encodePrompt(text), (error) => { - if (error) reject(error); - else resolve(); - }); - }); - }, - async stop(reason) { - if (process.platform === "win32") await cleanup; - if (state === "exited") return; - if (state === "starting") rejectReady(new AntigravityError("Antigravity startup canceled")); - clearTimeout(startupDeadline); - state = "stopping"; - if (process.platform === "win32") { - // Node signal emulation kills only the leader; taskkill must see the live tree. - cleanup = Promise.resolve(signalGroup(child, "SIGKILL")); - await cleanup; - await exited; - return; - } - if (reason === "close") child.stdin.end(); - else signalGroup(child, "SIGINT"); - const terminate = setTimeout(() => signalGroup(child, "SIGTERM"), 1000); - const kill = setTimeout(() => signalGroup(child, "SIGKILL"), 2000); - await exited; - clearTimeout(terminate); - clearTimeout(kill); - // A tool can keep running after its CLI leader exits. - signalGroup(child, "SIGKILL"); - }, - }; -} - -function signalGroup( - child: ChildProcessWithoutNullStreams, - signal: NodeJS.Signals, -): void | Promise { - if (!child.pid) return; - return signalProcess({ platform: process.platform, pid: child.pid, signal }); -} - -interface ProbeOptions { - launch: ProviderLaunch; - args: string[]; - cwd?: string; -} -export function probe(options: ProbeOptions): Promise { - return new Promise((resolve, reject) => { - const child = spawn(options.launch.command, [...options.launch.args, ...options.args], { - env: options.launch.env, - cwd: options.cwd, - stdio: "pipe", - detached: process.platform !== "win32", - }); - let stdout = ""; - let stderr = ""; - const deadline = setTimeout(() => { - const termination = Promise.resolve(signalGroup(child, "SIGKILL")); - void termination.then(() => { - reject(new AntigravityError("Antigravity discovery timed out", "PROBE_TIMEOUT")); - return undefined; - }, reject); - }, 10_000); - child.stdout.on("data", (chunk: Buffer) => { - stdout += chunk.toString(); - }); - child.stderr.on("data", (chunk: Buffer) => { - stderr = (stderr + chunk.toString()).slice(-8192); - }); - child.on("error", (error: Error) => { - clearTimeout(deadline); - reject(new AntigravityError(error.message, "SPAWN_ERROR")); - }); - child.on("close", (code) => { - clearTimeout(deadline); - if (code === 0) resolve(stdout); - else - reject( - new AntigravityError( - diagnostic(stderr.trim() || `agy exited with code ${code}`), - "PROBE_ERROR", - ), - ); - }); - }); -} diff --git a/plugins/antigravity-provider/server/internal/prompt.ts b/plugins/antigravity-provider/server/internal/prompt.ts deleted file mode 100644 index 07f4298b35..0000000000 --- a/plugins/antigravity-provider/server/internal/prompt.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { randomUUID } from "node:crypto"; -import { chmod, mkdtemp, rm, writeFile } from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import type { ProviderPrompt } from "@getpaseo/plugin/server/provider"; -import { AntigravityError } from "./wire.js"; - -interface EncodedPrompt { - text: string; - nativeText: string; -} - -export class PromptFiles { - private directory: string | null = null; - - async encode(prompt: ProviderPrompt): Promise { - if ( - prompt.input.type !== "message" || - prompt.delivery === "steer" || - prompt.outputSchema !== undefined - ) - throw new AntigravityError("Antigravity supports messages only"); - const parts = prompt.input.content.map((part) => { - if (part.type === "text") return part; - if (part.type === "image") return { ...part, extension: imageExtension(part.mimeType) }; - throw new AntigravityError("Antigravity supports text and image content only"); - }); - const text: string[] = []; - const native: string[] = []; - for (const part of parts) { - if (part.type === "text") { - text.push(part.text); - native.push(part.text); - } else if (part.type === "image") { - const extension = part.extension; - if (this.directory === null) { - this.directory = await mkdtemp(path.join(os.tmpdir(), "paseo-antigravity-images-")); - await chmod(this.directory, 0o700); - } - const file = path.join(this.directory, `${randomUUID()}.${extension}`); - await writeFile(file, Buffer.from(part.data, "base64"), { flag: "wx", mode: 0o600 }); - await chmod(file, 0o600); - native.push(`Attached image: ${file}`); - } - } - return { text: text.join("\n"), nativeText: native.join("\n") }; - } - - async close(): Promise { - if (this.directory !== null) { - await rm(this.directory, { recursive: true, force: true }); - this.directory = null; - } - } -} - -function imageExtension(mimeType: string): string { - const match = /^image\/([a-z0-9][a-z0-9.+-]*)$/i.exec(mimeType); - if (!match) throw new AntigravityError(`Expected an image MIME type, received ${mimeType}`); - const subtype = match[1].toLowerCase(); - if (subtype === "jpeg") return "jpg"; - if (subtype === "svg+xml") return "svg"; - return subtype; -} diff --git a/plugins/antigravity-provider/server/internal/session.ts b/plugins/antigravity-provider/server/internal/session.ts deleted file mode 100644 index 7121b516b5..0000000000 --- a/plugins/antigravity-provider/server/internal/session.ts +++ /dev/null @@ -1,382 +0,0 @@ -import { randomUUID } from "node:crypto"; -import { z } from "zod"; -import type { - ProviderCatalog, - ProviderConfigChanges, - ProviderEvent, - ProviderPersistence, - ProviderPrompt, - ProviderSessionConfig, - ProviderLaunch, - ProviderTimelineItem, - ProviderUsage, -} from "@getpaseo/plugin/server/provider"; -import { startDriver, selection, type Driver } from "./process.js"; -import { validateSelection } from "./catalog.js"; -import { PromptFiles } from "./prompt.js"; -import { toolItem } from "./timeline.js"; -import { AntigravityError, diagnostic, type Frame, type Step } from "./wire.js"; - -const persistenceSchema = z - .object({ version: z.literal(1), data: z.object({ conversationId: z.string().min(1) }).strict() }) - .strict(); -interface Turn { - id: string; - text: Map; - usageSteps: Set; - tools: Map>; - usage: Required>; -} -type State = - | { type: "dormant" } - | { type: "starting"; driver: Driver } - | { type: "idle"; driver: Driver } - | { type: "running"; driver: Driver; turn: Turn } - | { type: "stopping"; driver: Driver } - | { type: "closed" }; -interface SessionOptions { - id: string; - config: ProviderSessionConfig; - launch: ProviderLaunch; - catalog(): ProviderCatalog; - persistence?: ProviderPersistence; - emit(event: ProviderEvent): void; -} - -export class Session { - private state: State = { type: "dormant" }; - private config: ProviderSessionConfig; - private conversationId: string | null; - private firstMessage: boolean; - private readonly promptFiles = new PromptFiles(); - private readonly reportedDenials = new Map(); - - constructor(private readonly options: SessionOptions) { - this.config = options.config; - this.conversationId = options.persistence - ? persistenceSchema.parse(options.persistence).data.conversationId - : null; - this.firstMessage = this.conversationId === null; - validateSelection(this.config, options.catalog()); - } - - async open(requestId: string, capabilities: readonly string[]): Promise { - await this.ensureDriver(); - this.emit({ - type: "session.opened", - requestId, - sessionId: this.options.id, - capabilities, - restoration: "core", - persistence: this.config.persist ? this.persistence() : undefined, - cwd: this.config.cwd, - title: this.config.title, - }); - this.publishConfig(); - this.emit({ type: "session.commands", sessionId: this.options.id, commands: [] }); - this.emit({ type: "session.ready", requestId, sessionId: this.options.id }); - } - - async prompt(prompt: ProviderPrompt): Promise { - if (this.state.type === "running") - throw new AntigravityError( - "Antigravity already has an active turn; wait for it to finish", - "TURN_ACTIVE", - ); - const { text, nativeText } = await this.promptFiles.encode(prompt); - await this.ensureDriver(); - if (this.state.type !== "idle") throw new AntigravityError("Antigravity session is not ready"); - const driver = this.state.driver; - const turn: Turn = { - id: randomUUID(), - text: new Map(), - usageSteps: new Set(), - tools: new Map(), - usage: { inputTokens: 0, outputTokens: 0, cachedInputTokens: 0 }, - }; - this.state = { type: "running", driver, turn }; - this.emit({ - type: "session.prompt_result", - sessionId: this.options.id, - clientMessageId: prompt.clientMessageId, - result: { type: "turn", turnId: turn.id }, - }); - this.emit({ - type: "session.turn", - sessionId: this.options.id, - turnId: turn.id, - state: "started", - }); - this.item({ - type: "user_message", - id: prompt.clientMessageId, - clientMessageId: prompt.clientMessageId, - text, - }); - const driverText = - this.firstMessage && this.config.systemPrompt - ? `${this.config.systemPrompt}\n\n${nativeText}` - : nativeText; - try { - await driver.prompt(driverText); - this.firstMessage = false; - } catch (error) { - if (!(error instanceof Error)) throw error; - this.failTurn(new AntigravityError(error.message)); - await driver.stop("interrupt"); - } - } - - configure(changes: ProviderConfigChanges): void { - const config = { - ...this.config, - model: changes.model === null ? undefined : (changes.model ?? this.config.model), - mode: changes.mode === null ? undefined : (changes.mode ?? this.config.mode), - thinkingOption: - changes.thinkingOption === null - ? undefined - : (changes.thinkingOption ?? this.config.thinkingOption), - }; - if (changes.settings && Object.keys(changes.settings).length > 0) - throw new AntigravityError("Antigravity has no configurable settings"); - validateSelection(config, this.options.catalog()); - this.config = config; - this.publishConfig(); - } - - async interrupt(): Promise { - if (this.state.type !== "running") return; - const { driver, turn } = this.state; - this.state = { type: "stopping", driver }; - await driver.stop("interrupt"); - this.finishTools(turn, "canceled"); - this.emit({ - type: "session.turn", - sessionId: this.options.id, - turnId: turn.id, - state: "canceled", - }); - this.state = { type: "dormant" }; - } - - async close(): Promise { - const state = this.state; - this.state = { type: "closed" }; - try { - if (state.type !== "dormant" && state.type !== "closed") await state.driver.stop("close"); - } finally { - await this.promptFiles.close(); - } - if (state.type === "running") { - this.finishTools(state.turn, "canceled"); - this.emit({ - type: "session.turn", - sessionId: this.options.id, - turnId: state.turn.id, - state: "canceled", - }); - } - } - - private async ensureDriver(): Promise { - if (this.state.type === "closed") throw new AntigravityError("Antigravity session is closed"); - if (this.state.type === "stopping") { - await this.state.driver.stop("interrupt"); - this.state = { type: "dormant" }; - } - if (this.state.type === "idle" && this.state.driver.selection !== selection(this.config)) { - const driver = this.state.driver; - this.state = { type: "stopping", driver }; - await driver.stop("close"); - this.state = { type: "dormant" }; - } - if (this.state.type !== "dormant") return; - this.reportedDenials.clear(); - const driver = startDriver({ - launch: this.options.launch, - config: this.config, - conversationId: this.conversationId, - onFrame: (frame) => this.accept(frame), - onExit: (error) => this.failTurn(error), - }); - this.state = { type: "starting", driver }; - try { - const init = await driver.ready; - if (this.conversationId !== null && init.conversation_id !== this.conversationId) - throw new AntigravityError( - "Antigravity restored a different conversation", - "INVALID_RESUME", - ); - this.conversationId = init.conversation_id; - this.state = { type: "idle", driver }; - if (this.config.persist) - this.emit({ - type: "session.persistence", - sessionId: this.options.id, - persistence: this.persistence(), - }); - } catch (error) { - this.state = { type: "dormant" }; - await driver.stop("interrupt"); - throw error; - } - } - - private accept(frame: Frame): void { - if (this.state.type !== "running") return; - const { turn, driver } = this.state; - if (frame.event === "step_update") { - this.acceptStep(frame.step_update, turn); - return; - } - if (frame.event !== "result") return; - const result = frame.result; - const denied = takeNewDenials(result.denied_actions, this.reportedDenials); - if (denied.length > 0) { - this.emit({ - type: "session.notice", - sessionId: this.options.id, - notice: { - id: `${turn.id}:denied`, - severity: "warning", - title: denialNoticeTitle(denied), - description: "Switch to Full access to allow shell commands.", - }, - }); - } - if (result.status === "ERROR") { - const canceled = result.error === "interrupted"; - this.finishTools(turn, canceled ? "canceled" : "failed"); - this.retire(driver); - this.emit({ - type: "session.turn", - sessionId: this.options.id, - turnId: turn.id, - state: canceled ? "canceled" : "failed", - error: { message: diagnostic(result.error) }, - }); - } else { - this.finishTools(turn, "completed"); - this.state = { type: "idle", driver }; - this.emit({ - type: "session.turn", - sessionId: this.options.id, - turnId: turn.id, - state: "completed", - }); - } - } - - private acceptStep(step: Step, turn: Turn): void { - const id = `${turn.id}:${step.step_index}`; - if (step.step_type === "agent_response") { - const previous = turn.text.get(step.step_index) || ""; - const text = previous + step.text_delta; - turn.text.set(step.step_index, text); - if (step.text_delta) this.item({ type: "assistant_message", id, messageId: id, text }); - if (step.state === "DONE" && step.usage && !turn.usageSteps.has(step.step_index)) { - turn.usageSteps.add(step.step_index); - turn.usage.inputTokens += step.usage.input_tokens; - turn.usage.outputTokens += step.usage.output_tokens; - turn.usage.cachedInputTokens += step.usage.cache_read_tokens; - this.emit({ - type: "session.usage", - sessionId: this.options.id, - turnId: turn.id, - usage: { ...turn.usage }, - }); - } - } else if (step.step_type === "tool" || step.step_type === "subagent") { - const item = toolItem(step, id); - turn.tools.set(id, item); - this.item(item); - } - } - - private failTurn(error: AntigravityError): void { - const state = this.state; - if (state.type !== "running" && state.type !== "idle") return; - this.retire(state.driver); - if (state.type === "running") { - this.finishTools(state.turn, "failed"); - this.emit({ - type: "session.turn", - sessionId: this.options.id, - turnId: state.turn.id, - state: "failed", - error: { message: error.message, code: error.code }, - }); - } else - this.emit({ - type: "session.runtime_failed", - sessionId: this.options.id, - error: { message: error.message, code: error.code }, - }); - } - - private retire(driver: Driver): void { - this.state = { type: "stopping", driver }; - void driver.stop("interrupt").then(() => { - if (this.state.type === "stopping" && this.state.driver === driver) - this.state = { type: "dormant" }; - return undefined; - }); - } - - private finishTools(turn: Turn, status: "completed" | "failed" | "canceled"): void { - for (const item of turn.tools.values()) { - if (item.status !== "running") continue; - if (status === "failed") this.item({ ...item, status, error: "Antigravity turn failed" }); - else this.item({ ...item, status, error: null }); - } - } - - private persistence(): ProviderPersistence { - if (this.conversationId === null) throw new AntigravityError("Antigravity has no conversation"); - return { version: 1, data: { conversationId: this.conversationId } }; - } - - private publishConfig(): void { - this.emit({ - type: "session.config", - sessionId: this.options.id, - config: { - model: this.config.model, - mode: this.config.mode || "default", - models: this.options.catalog().models, - modes: this.options.catalog().modes, - thinkingOptions: [], - settings: [], - }, - }); - } - private item(item: ProviderTimelineItem): void { - this.emit({ type: "timeline.item", sessionId: this.options.id, item }); - } - private emit(event: ProviderEvent): void { - this.options.emit(event); - } -} - -// Native results contain every denial since this driver started, including repeated actions. -function takeNewDenials( - actions: Extract["result"]["denied_actions"], - reported: Map, -) { - const counts = new Map(); - const fresh = actions.filter((action) => { - const key = JSON.stringify([action.action, action.display_name]); - const count = (counts.get(key) ?? 0) + 1; - counts.set(key, count); - return count > (reported.get(key) ?? 0); - }); - for (const [key, count] of counts) reported.set(key, Math.max(count, reported.get(key) ?? 0)); - return fresh; -} - -function denialNoticeTitle( - actions: Extract["result"]["denied_actions"], -): string { - if (actions.length > 1) return `${actions.length} actions denied`; - return actions[0].action === "command" ? "Shell command denied" : "Action denied"; -} diff --git a/plugins/antigravity-provider/server/internal/signals.ts b/plugins/antigravity-provider/server/internal/signals.ts deleted file mode 100644 index be34ecc8c9..0000000000 --- a/plugins/antigravity-provider/server/internal/signals.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { execFile } from "node:child_process"; -import { promisify } from "node:util"; - -interface SignalOptions { - platform: NodeJS.Platform; - pid: number; - signal: NodeJS.Signals; -} - -type SignalPlan = - | { type: "group"; pid: number; signal: NodeJS.Signals } - | { type: "tree"; command: "taskkill"; args: string[] }; - -const executeFile = promisify(execFile); - -export function signalPlan(options: SignalOptions): SignalPlan { - if (options.platform === "win32") { - return { type: "tree", command: "taskkill", args: ["/PID", String(options.pid), "/T", "/F"] }; - } - return { type: "group", pid: -options.pid, signal: options.signal }; -} - -export function signalProcess(options: SignalOptions): void | Promise { - const plan = signalPlan(options); - if (plan.type === "tree") return killTree(plan); - try { - process.kill(plan.pid, plan.signal); - } catch (error) { - if (!(error instanceof Error && "code" in error && error.code === "ESRCH")) throw error; - } -} - -async function killTree(plan: Extract): Promise { - try { - await executeFile(plan.command, plan.args, { windowsHide: true }); - } catch (error) { - // taskkill reports an already-exited PID with exit code 128. - if (!(error instanceof Error && "code" in error && error.code === 128)) throw error; - } -} diff --git a/plugins/antigravity-provider/server/internal/timeline.ts b/plugins/antigravity-provider/server/internal/timeline.ts deleted file mode 100644 index 22cc81c643..0000000000 --- a/plugins/antigravity-provider/server/internal/timeline.ts +++ /dev/null @@ -1,55 +0,0 @@ -import type { - ProviderTimelineItem, - ProviderToolCallDetail, -} from "@getpaseo/plugin/server/provider"; -import type { Step } from "./wire.js"; - -export function toolItem( - step: Step, - id: string, -): Extract { - const detail = toolDetail(step); - const name = step.tool_name || "Antigravity tool"; - const metadata = step.subagent_info ? { subagents: step.subagent_info.subagents } : undefined; - const base = { type: "tool_call" as const, id, callId: id, name, detail, metadata }; - if (step.state === "ERROR") { - const error = - step.tool_info && step.tool_info.error - ? step.tool_info.error.message - : "Antigravity tool failed"; - return { ...base, status: "failed", error }; - } - const status = step.state === "ACTIVE" ? "running" : "completed"; - return { ...base, status, error: null }; -} - -function toolDetail(step: Step): ProviderToolCallDetail { - if (step.subagent_info) { - const child = step.subagent_info.subagents[0]; - if (child) - return { - type: "sub_agent", - subAgentType: child.type_name, - description: child.initial_prompt, - childSessionId: child.conversation_id === null ? undefined : child.conversation_id, - log: child.log_uri, - }; - } - const info = step.tool_info; - if (!info) return { type: "plain_text", label: step.tool_name, text: "" }; - const path = info.parameters.AbsolutePath || info.parameters.TargetFile; - if (info.name === "run_command" && typeof info.parameters.CommandLine === "string") { - return { type: "shell", command: info.parameters.CommandLine, output: info.output }; - } - if (info.name === "view_file" && typeof path === "string") - return { type: "read", filePath: path, content: info.output }; - if (info.name === "write_to_file" && typeof path === "string") - return { type: "write", filePath: path }; - if ( - (info.name === "replace_file_content" || info.name === "multi_replace_file_content") && - typeof path === "string" - ) - return { type: "edit", filePath: path }; - // Native frames summarize writes; they do not carry the file contents or diff. - return { type: "unknown", input: info.parameters, output: info.error || info.output }; -} diff --git a/plugins/antigravity-provider/server/internal/wire.ts b/plugins/antigravity-provider/server/internal/wire.ts deleted file mode 100644 index eef417c83c..0000000000 --- a/plugins/antigravity-provider/server/internal/wire.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { z } from "zod"; - -const usage = z.object({ - input_tokens: z.number().nonnegative(), - output_tokens: z.number().nonnegative(), - cache_read_tokens: z.number().nonnegative().default(0), -}); -const tool = z.object({ - name: z.string(), - parameters: z.record(z.string(), z.json()).default({}), - output: z.string().default(""), - error: z.object({ type: z.string(), message: z.string() }).nullable().default(null), -}); -const child = z.object({ - type_name: z.string(), - role: z.string().default(""), - initial_prompt: z.string().default(""), - conversation_id: z.string().nullable().default(null), - log_uri: z.string().default(""), - workspace_uris: z.array(z.string()).default([]), -}); -const frame = z.discriminatedUnion("event", [ - z.object({ - event: z.literal("init"), - conversation_id: z.string().min(1), - init: z.object({ model: z.string().nullable().default(null) }), - }), - z.object({ - event: z.literal("step_update"), - step_update: z.object({ - conversation_id: z.string(), - step_index: z.number().int().nonnegative(), - state: z.enum(["ACTIVE", "DONE", "ERROR"]), - step_type: z.string(), - text_delta: z.string().default(""), - tool_name: z.string().default(""), - tool_info: tool.nullable().default(null), - subagent_info: z - .object({ subagents: z.array(child) }) - .nullable() - .default(null), - usage: usage.nullable().default(null), - }), - }), - z.object({ - event: z.literal("result"), - result: z.object({ - conversation_id: z.string(), - status: z.enum(["SUCCESS", "ERROR"]), - response: z.string().default(""), - error: z.string().default("Antigravity turn failed"), - denied_actions: z - .array(z.object({ action: z.string(), display_name: z.string() })) - .default([]), - }), - }), -]); -export type Frame = z.infer; -export type Step = Extract["step_update"]; -export type Init = Extract; - -export function decodeFrame(line: string): Frame { - return frame.parse(JSON.parse(line)); -} - -export function encodePrompt(text: string): string { - return `${JSON.stringify({ event: "user", message: { content: text } })}\n`; -} - -export class AntigravityError extends Error { - constructor( - message: string, - readonly code = "ANTIGRAVITY_ERROR", - ) { - super(message); - this.name = "AntigravityError"; - } -} - -export function diagnostic(message: string): string { - if (/auth|sign.in|log.in/i.test(message)) { - return "Antigravity authentication failed. Run `agy` and sign in, then retry."; - } - return message; -} diff --git a/plugins/antigravity-provider/server/provider.test.ts b/plugins/antigravity-provider/server/provider.test.ts deleted file mode 100644 index b9a478c7c6..0000000000 --- a/plugins/antigravity-provider/server/provider.test.ts +++ /dev/null @@ -1,717 +0,0 @@ -import { mkdtemp, readFile, rm, stat } from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; -import { afterEach, describe, expect, it } from "vitest"; -import { - PROVIDER_CAPABILITIES, - ProviderEventSchema, - type ProviderConnection, - type ProviderContent, - type ProviderEvent, - type ProviderInput, - type ProviderLaunch, - type ProviderPersistence, -} from "@getpaseo/plugin/server/provider"; -import { signalPlan } from "./internal/signals.js"; -import { createAntigravityProvider } from "./provider.js"; - -const temporary: string[] = []; -const connections: ProviderConnection[] = []; -const fake = fileURLToPath(new URL("../test/agy.mjs", import.meta.url)); -const provider = createAntigravityProvider(); - -it("publishes a monochrome, theme-aware provider icon", async () => { - const icon = await readFile(new URL("../icon.svg", import.meta.url), "utf8"); - expect(icon).toContain('viewBox="0 0 24 24"'); - expect(icon).toContain('fill="currentColor"'); - expect(icon).not.toMatch(/#[\da-f]{3,8}\b|rgba?\(|hsla?\(|url\(/i); - expect(icon).not.toMatch(/<(?:style|mask|defs|linearGradient|radialGradient)\b|\bid=/i); - expect([...icon.matchAll(/\b(?:fill|stroke)="([^"]+)"/g)].map((match) => match[1])).toEqual([ - "currentColor", - ]); -}); - -afterEach(async () => { - await Promise.all(connections.splice(0).map((connection) => connection.close())); - await Promise.all( - temporary.splice(0).map((directory) => rm(directory, { recursive: true, force: true })), - ); -}); - -async function harness(env: Record = {}) { - const cwd = await mkdtemp(path.join(os.tmpdir(), "antigravity-test-")); - temporary.push(cwd); - const log = path.join(cwd, "argv.ndjson"); - const launch: ProviderLaunch = { - command: process.execPath, - args: [fake, "--launch-prefix"], - env: { AGY_TEST_LOG: log, ...env }, - }; - const connection = await provider.connect({ - versions: [1], - capabilities: PROVIDER_CAPABILITIES, - launch, - }); - connections.push(connection); - const events: ProviderEvent[] = []; - connection.onEvent((event) => { - ProviderEventSchema.parse(event); - events.push(event); - }); - async function wait(predicate: (event: ProviderEvent) => boolean) { - await expect.poll(() => events.some(predicate), { timeout: 5000, interval: 10 }).toBe(true); - const event = events.find(predicate); - if (!event) throw new Error("Expected event missing"); - return event; - } - async function request(input: ProviderInput) { - await connection.send(input); - if ("requestId" in input) - await wait( - (event) => - "requestId" in event && - event.requestId === input.requestId && - (event.type === "request.completed" || - event.type === "request.failed" || - event.type === "session.ready" || - event.type === "catalog"), - ); - } - async function open(restored?: ProviderPersistence, sessionId = "s") { - await request({ - type: "session.open", - requestId: `open-${events.length}`, - sessionId, - persistence: restored, - history: "replay", - config: { - cwd, - env: { AGY_SESSION_VALUE: "session-overlay" }, - settings: {}, - mcpServers: {}, - persist: true, - systemPrompt: "SYSTEM PREFIX", - }, - }); - } - async function message(content: ProviderContent[], sessionId = "s") { - const clientMessageId = `prompt-${events.length}`; - await connection.send({ - type: "session.prompt", - sessionId, - prompt: { - clientMessageId, - delivery: "auto", - input: { type: "message", content }, - }, - }); - await wait( - (event) => - event.type === "session.prompt_result" && event.clientMessageId === clientMessageId, - ); - return clientMessageId; - } - function prompt(text: string) { - return message([{ type: "text", text }]); - } - async function completed(clientMessageId: string) { - const result = events.find( - (event) => - event.type === "session.prompt_result" && event.clientMessageId === clientMessageId, - ); - if (!result || result.type !== "session.prompt_result" || result.result.type !== "turn") - throw new Error("Prompt was not admitted"); - const turnId = result.result.turnId; - return wait( - (event) => - event.type === "session.turn" && event.turnId === turnId && event.state !== "started", - ); - } - async function records() { - return (await readFile(log, "utf8")) - .trim() - .split("\n") - .map((line) => JSON.parse(line)); - } - function persistence() { - const opened = events.find((event) => event.type === "session.opened"); - if (!opened || opened.type !== "session.opened" || !opened.persistence) - throw new Error("Missing persistence"); - return opened.persistence; - } - return { - connection, - events, - launch, - cwd, - wait, - request, - open, - prompt, - message, - completed, - records, - persistence, - }; -} - -it("streams complete text snapshots, prefixes the system prompt only once and preserves launch argv/env", async () => { - const h = await harness(); - expect(h.connection.capabilities).toEqual([ - "prompt.message", - "prompt.image", - "session.configure", - "session.persistence", - ]); - await h.open(); - await h.completed(await h.prompt("HELLO")); - await h.completed(await h.prompt("HELLO AGAIN")); - const text = h.events.filter( - (event) => event.type === "timeline.item" && event.item.type === "assistant_message", - ); - expect( - text.map((event) => - event.type === "timeline.item" && event.item.type === "assistant_message" - ? event.item.text - : "", - ), - ).toEqual(["PHASE0_HELLO", "PHASE0_HELLO\n", "PHASE0_HELLO", "PHASE0_HELLO\n"]); - const records = await h.records(); - expect(records.find((entry) => entry.args.includes("--input-format")).args).toEqual([ - "--launch-prefix", - "--input-format", - "stream-json", - "--output-format", - "stream-json", - "--add-dir", - h.cwd, - "--disable-slash-commands", - ]); - expect(records.find((entry) => entry.args.includes("--input-format")).env).toBe( - "session-overlay", - ); - expect( - records.filter((entry) => entry.input).map((entry) => entry.input.message.content), - ).toEqual(["SYSTEM PREFIX\n\nHELLO", "HELLO AGAIN"]); -}); - -it("maps tools and emits one denial notice for a SUCCESS result", async () => { - const h = await harness(); - await h.open(); - // This capture contains three distinct native results, one for each submitted prompt. - await h.completed(await h.prompt("TOOLS")); - await h.completed(await h.prompt("TOOLS")); - const result = await h.completed(await h.prompt("TOOLS")); - expect(result).toMatchObject({ type: "session.turn", state: "completed" }); - const tools = h.events.filter( - (event) => event.type === "timeline.item" && event.item.type === "tool_call", - ); - expect(tools).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - item: expect.objectContaining({ - name: "view_file", - status: "completed", - detail: expect.objectContaining({ type: "read" }), - }), - }), - expect.objectContaining({ - item: expect.objectContaining({ - name: "write_to_file", - status: "completed", - detail: expect.objectContaining({ type: "write" }), - }), - }), - expect.objectContaining({ - item: expect.objectContaining({ - name: "run_command", - status: "failed", - detail: expect.objectContaining({ type: "shell", command: "printf PHASE0_SHELL" }), - }), - }), - ]), - ); - const notices = h.events.filter((event) => event.type === "session.notice"); - expect(notices).toHaveLength(1); - expect(notices[0]).toMatchObject({ - notice: { - title: "Shell command denied", - description: "Switch to Full access to allow shell commands.", - }, - }); -}); - -it("reports only new process denials across replayed turns and resets after respawn", async () => { - const h = await harness(); - await h.open(); - await h.completed(await h.prompt("DENIAL_REPLAY")); - expect(h.events.filter((event) => event.type === "session.notice")).toHaveLength(1); - const followupStart = h.events.length; - await h.completed(await h.prompt("DENIAL_REPLAY")); - const followup = h.events.slice(followupStart); - expect(followup.some((event) => event.type === "session.notice")).toBe(false); - expect(followup).toContainEqual( - expect.objectContaining({ - type: "timeline.item", - item: expect.objectContaining({ type: "assistant_message", text: "FOLLOWUP_OK\n" }), - }), - ); - await h.request({ - type: "session.configure", - sessionId: "s", - requestId: "mode", - changes: { mode: "plan" }, - }); - await h.completed(await h.prompt("DENIAL_REPLAY")); - expect(h.events.filter((event) => event.type === "session.notice")).toHaveLength(2); - expect( - (await h.records()).filter((entry) => entry.args?.includes("--input-format")), - ).toHaveLength(2); -}); - -it("counts repeated native action denials without exposing tool identifiers", async () => { - const h = await harness({ AGY_TEST_DENIAL_COUNTS: "2,2,3" }); - await h.open(); - await h.completed(await h.prompt("DENIAL_REPLAY")); - await h.completed(await h.prompt("DENIAL_REPLAY")); - const notices = h.events.filter((event) => event.type === "session.notice"); - expect(notices).toHaveLength(1); - expect(notices[0]).toMatchObject({ - notice: { - title: "2 actions denied", - description: "Switch to Full access to allow shell commands.", - }, - }); - await h.completed(await h.prompt("DENIAL_REPLAY")); - expect(h.events.filter((event) => event.type === "session.notice")).toHaveLength(2); - expect(h.events.findLast((event) => event.type === "session.notice")).toMatchObject({ - notice: { title: "Shell command denied" }, - }); -}); - -it("accounts DONE generation usage once without adding cumulative results, including resume", async () => { - const h = await harness(); - await h.open(); - await h.completed(await h.prompt("MULTI")); - await h.completed(await h.prompt("MULTI")); - const usages = h.events.filter((event) => event.type === "session.usage"); - expect( - usages.map((event) => (event.type === "session.usage" ? event.usage.inputTokens : null)), - ).toEqual([11263, 11604]); - const persistence = h.persistence(); - await h.request({ type: "session.close", sessionId: "s", requestId: "close" }); - await h.open(persistence); - await h.completed(await h.prompt("RESUME")); - expect(h.events.filter((event) => event.type === "session.usage")).toHaveLength(3); - expect(h.events.findLast((event) => event.type === "session.usage")).toMatchObject({ - usage: { inputTokens: 11815 }, - }); -}); - -it("maps subagent steps to a tool carrying the child conversation ID", async () => { - const h = await harness(); - await h.open(); - await h.completed(await h.prompt("SUBAGENT")); - expect(h.events).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - type: "timeline.item", - item: expect.objectContaining({ - name: "invoke_subagent", - status: "completed", - detail: expect.objectContaining({ - type: "sub_agent", - childSessionId: "b6f02329-7415-49a7-a9a9-1cd09e898d26", - }), - }), - }), - ]), - ); -}); - -it.each(["HANG", "TOOL_HANG"])( - "interrupts %s, settles canceled, and respawns the same conversation", - async (text) => { - const h = await harness(); - await h.open(); - const message = await h.prompt(text); - await h.wait( - (event) => - event.type === "timeline.item" && - (event.item.type === "assistant_message" || event.item.type === "tool_call"), - ); - await h.request({ type: "session.interrupt", requestId: "interrupt", sessionId: "s" }); - expect(await h.completed(message)).toMatchObject({ state: "canceled" }); - await h.completed(await h.prompt("HELLO")); - const records = await h.records(); - const launches = records.filter((entry) => entry.args && entry.args.includes("--input-format")); - expect(launches).toHaveLength(2); - expect(launches[1].args.slice(-2)).toEqual([ - "--conversation", - "e5cf1e2d-c715-4328-8e67-4a85c8dc3cda", - ]); - // Windows taskkill terminates the tree without invoking the CLI's signal handlers. - expect(records.filter((entry) => entry.signal)).toEqual( - process.platform === "win32" ? [] : [{ signal: "SIGINT" }], - ); - const canceled = h.events.filter( - (event) => event.type === "session.turn" && event.state === "canceled", - ); - expect(canceled).toHaveLength(1); - }, -); - -it("stops an unresponsive driver and still respawns the conversation", async () => { - const h = await harness({ AGY_TEST_IGNORE_SIGNALS: "yes" }); - await h.open(); - const message = await h.prompt("HANG"); - await h.wait( - (event) => event.type === "timeline.item" && event.item.type === "assistant_message", - ); - await h.request({ type: "session.interrupt", requestId: "interrupt-timeout", sessionId: "s" }); - expect(await h.completed(message)).toMatchObject({ state: "canceled" }); - expect((await h.records()).filter((entry) => entry.signal)).toEqual( - process.platform === "win32" ? [] : [{ signal: "SIGINT" }, { signal: "SIGTERM" }], - ); - await h.completed(await h.prompt("HELLO")); -}); - -it.each(["accept-edits", "plan"])( - "launches native %s mode without claiming read-only access", - async (mode) => { - const h = await harness(); - await h.open(); - await h.request({ - type: "session.configure", - requestId: "mode", - sessionId: "s", - changes: { mode }, - }); - await h.completed(await h.prompt("HELLO")); - const launches = (await h.records()).filter( - (entry) => entry.args && entry.args.includes("--input-format"), - ); - expect(launches[1].args).toContain("--mode"); - expect(launches[1].args[launches[1].args.indexOf("--mode") + 1]).toBe(mode); - }, -); - -it("defers model and mode changes until the next prompt, then respawns with validated flags", async () => { - const h = await harness(); - await h.open(); - await h.request({ - type: "session.configure", - requestId: "change", - sessionId: "s", - changes: { model: "gemini-3.8-flash-low", mode: "full-access" }, - }); - expect( - (await h.records()).filter((entry) => entry.args && entry.args.includes("--input-format")), - ).toHaveLength(1); - await h.completed(await h.prompt("HELLO")); - const launches = (await h.records()).filter( - (entry) => entry.args && entry.args.includes("--input-format"), - ); - expect(launches).toHaveLength(2); - expect(launches[1].args.slice(-5)).toEqual([ - "--model", - "gemini-3.8-flash-low", - "--dangerously-skip-permissions", - "--conversation", - "e5cf1e2d-c715-4328-8e67-4a85c8dc3cda", - ]); - await h.request({ - type: "session.configure", - requestId: "invalid", - sessionId: "s", - changes: { model: "unknown" }, - }); - expect(h.events).toContainEqual( - expect.objectContaining({ - type: "request.failed", - requestId: "invalid", - error: expect.objectContaining({ code: "INVALID_MODEL" }), - }), - ); -}); - -it("resumes after connection close without replay or another system prefix", async () => { - const first = await harness(); - await first.open(); - await first.completed(await first.prompt("HELLO")); - const persistence = first.persistence(); - await first.connection.close(); - const second = await harness(); - await second.open(persistence); - expect(second.events.filter((event) => event.type === "timeline.item")).toEqual([]); - await second.completed(await second.prompt("RESUME")); - expect( - (await second.records()) - .filter((entry) => entry.input) - .map((entry) => entry.input.message.content), - ).toEqual(["RESUME"]); -}); - -it.each(["auth", "exit", "stderr"])( - "reports startup %s without waiting for init forever", - async (startup) => { - const h = await harness({ AGY_TEST_STARTUP: startup }); - await h.open(); - expect(h.events.filter((event) => event.type === "session.ready")).toEqual([]); - const failure = h.events.find((event) => event.type === "request.failed"); - expect(failure).toMatchObject({ - type: "request.failed", - error: { - message: expect.stringMatching(startup === "exit" ? /exited/ : /Run `agy` and sign in/), - }, - }); - }, -); - -it("rejects mid-turn steering and reports a busy message without queueing a second native turn", async () => { - const h = await harness(); - await h.open(); - await h.prompt("HANG"); - const second = await h.prompt("SECOND"); - expect( - h.events.find( - (event) => event.type === "session.prompt_result" && event.clientMessageId === second, - ), - ).toMatchObject({ - type: "session.prompt_result", - result: { type: "failed", error: { code: "TURN_ACTIVE" } }, - }); - await expect( - h.connection.send({ - type: "session.prompt", - sessionId: "s", - prompt: { - clientMessageId: "steer", - delivery: "steer", - input: { type: "message", content: [{ type: "text", text: "x" }] }, - }, - }), - ).rejects.toThrow(/prompt.steer/); -}); - -describe("discovery", () => { - it("reuses one discovered catalog for two opens and configure, and refreshes explicit catalog requests", async () => { - const h = await harness(); - await h.open(); - await h.open(undefined, "second"); - await h.request({ - type: "session.configure", - requestId: "configure-cached", - sessionId: "second", - changes: { model: "gemini-3.8-flash-low" }, - }); - expect( - (await h.records()).filter((entry) => entry.args && entry.args.includes("models")), - ).toHaveLength(1); - await h.request({ type: "catalog", requestId: "refresh" }); - expect( - (await h.records()).filter((entry) => entry.args && entry.args.includes("models")), - ).toHaveLength(2); - }); - it.each(["1.1.15", "1.2.12", "1.2.13"])("accepts driver-capable version %s", async (version) => { - const h = await harness({ AGY_TEST_VERSION: version }); - expect(await provider.status?.({ launch: h.launch })).toEqual({ available: true }); - }); - - it("parses tab-separated catalog and exposes native permission choices without thinking options", async () => { - const h = await harness(); - await h.request({ type: "catalog", requestId: "catalog" }); - const event = h.events[0]; - expect(event).toMatchObject({ - type: "catalog", - catalog: { - models: expect.arrayContaining([ - { id: "gemini-3.8-flash-low", label: "Gemini 3.8 Flash (Low)" }, - ]), - thinkingOptions: [], - defaultMode: "default", - }, - }); - if (event.type !== "catalog") throw new Error("Missing catalog"); - expect(event.catalog.models).toHaveLength(11); - expect(event.catalog.modes.map((mode) => mode.label)).toEqual([ - "Default", - "Accept edits", - "Plan", - "Full access", - ]); - expect(event.catalog.modes.map((mode) => mode.id)).toEqual([ - "default", - "accept-edits", - "plan", - "full-access", - ]); - expect( - event.catalog.modes.map(({ id, icon, colorTier, isUnattended }) => ({ - id, - icon, - colorTier, - isUnattended, - })), - ).toEqual([ - { id: "default", icon: "Shield", colorTier: "moderate", isUnattended: undefined }, - { id: "accept-edits", icon: "ShieldPlus", colorTier: "moderate", isUnattended: undefined }, - { id: "plan", icon: "ShieldEllipsis", colorTier: "planning", isUnattended: undefined }, - { id: "full-access", icon: "ShieldOff", colorTier: "dangerous", isUnattended: true }, - ]); - }); - it("reports missing, old, unauthenticated and available launches", async () => { - expect(await provider.status?.({})).toMatchObject({ - available: false, - diagnostic: expect.stringContaining("agy not found"), - }); - const missing = await harness(); - expect( - await provider.status?.({ - launch: { ...missing.launch, command: path.join(missing.cwd, "missing-agy") }, - }), - ).toMatchObject({ available: false, diagnostic: expect.stringContaining("ENOENT") }); - const old = await harness({ AGY_TEST_VERSION: "1.1.14" }); - expect(await provider.status?.({ launch: old.launch })).toMatchObject({ - available: false, - diagnostic: expect.stringContaining("1.1.15"), - }); - const unauthenticated = await harness({ AGY_TEST_AUTH: "missing" }); - expect(await provider.status?.({ launch: unauthenticated.launch })).toEqual({ - available: false, - diagnostic: "Antigravity authentication failed. Run `agy` and sign in, then retry.", - }); - const authenticated = await harness(); - expect(await provider.status?.({ launch: authenticated.launch })).toEqual({ available: true }); - }); - it("keys catalog caching by the complete effective launch without exposing env values", async () => { - const h = await harness(); - const key = await provider.getCatalogCacheKey?.({ scope: "global", launch: h.launch }); - expect(key).toMatch(/^[a-f0-9]{64}$/); - expect( - await provider.getCatalogCacheKey?.({ - scope: "workspace", - cwd: h.cwd, - force: true, - launch: h.launch, - }), - ).toBe(key); - expect( - await provider.getCatalogCacheKey?.({ - scope: "global", - launch: { ...h.launch, args: [...h.launch.args, "changed"] }, - }), - ).not.toBe(key); - expect( - await provider.getCatalogCacheKey?.({ - scope: "global", - launch: { ...h.launch, env: { ...h.launch.env, HOME: "different" } }, - }), - ).not.toBe(key); - }); -}); - -describe("process signal plans", () => { - it.each(["SIGINT", "SIGTERM", "SIGKILL"] as const)("kills the Windows tree for %s", (signal) => { - expect(signalPlan({ platform: "win32", pid: 123, signal })).toEqual({ - type: "tree", - command: "taskkill", - args: ["/PID", "123", "/T", "/F"], - }); - }); - it.each(["SIGINT", "SIGTERM", "SIGKILL"] as const)( - "preserves POSIX group signal %s", - (signal) => { - expect(signalPlan({ platform: "linux", pid: 123, signal })).toEqual({ - type: "group", - pid: -123, - signal, - }); - }, - ); -}); - -describe("image prompts", () => { - it.each(["session", "connection"])( - "encodes mixed content as private session files and cleans up on %s close", - async (close) => { - const h = await harness(); - await h.open(); - const png = await readFile( - new URL("../test/fixtures/image-file-external.png", import.meta.url), - ); - const jpeg = Buffer.from("image file bytes"); - await h.completed( - await h.message([ - { type: "text", text: "Describe these images." }, - { type: "image", data: png.toString("base64"), mimeType: "image/png" }, - { type: "text", text: "Compare them." }, - { type: "image", data: jpeg.toString("base64"), mimeType: "image/jpeg" }, - ]), - ); - const sent = (await h.records()).find((entry) => entry.input).input.message.content; - const lines = sent.split("\n"); - const pngPath = lines[3].slice("Attached image: ".length); - const jpegPath = lines[5].slice("Attached image: ".length); - expect(sent).toBe( - `SYSTEM PREFIX\n\nDescribe these images.\nAttached image: ${pngPath}\nCompare them.\nAttached image: ${jpegPath}`, - ); - expect(path.isAbsolute(pngPath)).toBe(true); - expect(path.extname(pngPath)).toBe(".png"); - expect(path.extname(jpegPath)).toBe(".jpg"); - expect(path.dirname(pngPath)).toBe(path.dirname(jpegPath)); - expect(path.dirname(pngPath)).not.toBe(h.cwd); - expect(await readFile(pngPath)).toEqual(png); - expect(await readFile(jpegPath)).toEqual(jpeg); - if (process.platform !== "win32") { - expect((await stat(path.dirname(pngPath))).mode & 0o777).toBe(0o700); - expect((await stat(pngPath)).mode & 0o777).toBe(0o600); - expect((await stat(jpegPath)).mode & 0o777).toBe(0o600); - } - const args = (await h.records()).find((entry) => entry.args?.includes("--input-format")).args; - expect(args.filter((arg: string) => arg === "--add-dir")).toHaveLength(1); - if (close === "connection") await h.connection.close(); - else await h.request({ type: "session.close", requestId: "close-images", sessionId: "s" }); - await expect(stat(path.dirname(pngPath))).rejects.toMatchObject({ code: "ENOENT" }); - }, - ); - - it("keeps image-only prompts in distinct session directories until each session closes", async () => { - const h = await harness(); - await h.open(); - await h.open(undefined, "other"); - const content: ProviderContent[] = [ - { type: "image", data: "aW1hZ2U=", mimeType: "image/webp" }, - ]; - await h.completed(await h.message(content)); - await h.completed(await h.message(content)); - await h.completed(await h.message(content, "other")); - const paths = (await h.records()) - .filter((entry) => entry.input) - .map((entry) => entry.input.message.content.split("Attached image: ")[1]); - expect(new Set(paths).size).toBe(3); - expect(path.dirname(paths[0])).toBe(path.dirname(paths[1])); - expect(path.dirname(paths[0])).not.toBe(path.dirname(paths[2])); - expect(paths.every((file: string) => path.extname(file) === ".webp")).toBe(true); - await h.request({ type: "session.close", requestId: "close-first", sessionId: "s" }); - await expect(stat(path.dirname(paths[0]))).rejects.toMatchObject({ code: "ENOENT" }); - expect(await readFile(paths[2], "utf8")).toBe("image"); - await h.connection.close(); - await expect(stat(path.dirname(paths[2]))).rejects.toMatchObject({ code: "ENOENT" }); - }); - - it("rejects a non-image MIME type before writing to native stdin", async () => { - const h = await harness(); - await h.open(); - const id = await h.message([{ type: "image", data: "dGV4dA==", mimeType: "text/plain" }]); - expect( - h.events.find( - (event) => event.type === "session.prompt_result" && event.clientMessageId === id, - ), - ).toMatchObject({ - result: { type: "failed", error: { message: expect.stringContaining("image MIME type") } }, - }); - expect((await h.records()).filter((entry) => entry.input)).toHaveLength(0); - }); -}); diff --git a/plugins/antigravity-provider/server/provider.ts b/plugins/antigravity-provider/server/provider.ts deleted file mode 100644 index 88c73c7faa..0000000000 --- a/plugins/antigravity-provider/server/provider.ts +++ /dev/null @@ -1,171 +0,0 @@ -import { createHash } from "node:crypto"; -import { - negotiateProviderCapabilities, - requireProviderCapabilities, - type ProviderCatalog, - type ProviderConnection, - type ProviderEvent, - type ProviderInput, - type ProviderLaunch, - type ProviderRegistration, -} from "@getpaseo/plugin/server/provider"; -import { getCatalog, getStatus } from "./internal/catalog.js"; -import { Session } from "./internal/session.js"; -import { AntigravityError } from "./internal/wire.js"; - -const capabilities = [ - "prompt.message", - "prompt.image", - "session.configure", - "session.persistence", -] as const; - -export function createAntigravityProvider(): ProviderRegistration { - return { - id: "antigravity", - label: "Antigravity", - icon: "icon.svg", - command: ["agy"], - description: "Antigravity CLI on your host", - async status({ launch }) { - if (!launch) - return { - available: false, - diagnostic: "agy not found on PATH. Install Antigravity, then run `agy` and sign in.", - }; - return getStatus(launch); - }, - async getCatalogCacheKey({ launch }) { - if (!launch) return undefined; - const env = Object.entries(launch.env).sort(([left], [right]) => left.localeCompare(right)); - return createHash("sha256") - .update(JSON.stringify([launch.command, launch.args, env])) - .digest("hex"); - }, - async connect(request) { - if (!request.versions.includes(1)) - throw new AntigravityError("Provider protocol version 1 is required"); - if (!request.launch) - throw new AntigravityError("The daemon must supply the Antigravity launch"); - const negotiated = negotiateProviderCapabilities(request.capabilities, capabilities); - return createConnection(request.launch, negotiated); - }, - }; -} - -function createConnection( - launch: ProviderLaunch, - negotiated: readonly string[], -): ProviderConnection { - const sessions = new Map(); - const listeners = new Set<(event: ProviderEvent) => void>(); - let discovered: ProviderCatalog | null = null; - let closed = false; - let pending = Promise.resolve(); - - function emit(event: ProviderEvent): void { - if (closed) return; - for (const listener of listeners) listener(event); - } - function catalog(): ProviderCatalog { - if (discovered === null) - throw new AntigravityError("Antigravity catalog has not been discovered"); - return discovered; - } - function session(id: string): Session { - const found = sessions.get(id); - if (!found) throw new AntigravityError(`Unknown Antigravity session: ${id}`); - return found; - } - async function dispatch(input: ProviderInput): Promise { - switch (input.type) { - case "catalog": - discovered = await getCatalog(launch, input.cwd); - emit({ - type: "catalog", - requestId: input.requestId, - catalog: catalog(), - }); - return; - case "session.open": { - if (sessions.has(input.sessionId)) - throw new AntigravityError(`Session already exists: ${input.sessionId}`); - if (discovered === null) discovered = await getCatalog(launch, input.config.cwd); - const opened = new Session({ - id: input.sessionId, - config: input.config, - persistence: input.persistence, - catalog, - launch, - emit, - }); - sessions.set(input.sessionId, opened); - try { - await opened.open(input.requestId, negotiated); - } catch (error) { - sessions.delete(input.sessionId); - await opened.close(); - throw error; - } - return; - } - case "session.prompt": - await session(input.sessionId).prompt(input.prompt); - return; - case "session.configure": - session(input.sessionId).configure(input.changes); - break; - case "session.interrupt": - await session(input.sessionId).interrupt(); - break; - case "session.close": { - await session(input.sessionId).close(); - sessions.delete(input.sessionId); - emit({ type: "session.closed", sessionId: input.sessionId }); - break; - } - default: - throw new AntigravityError(`Unsupported Antigravity operation: ${input.type}`); - } - emit({ type: "request.completed", requestId: input.requestId }); - } - function failed(input: ProviderInput, error: unknown): void { - const message = error instanceof Error ? error.message : String(error); - const code = error instanceof AntigravityError ? error.code : "ANTIGRAVITY_ERROR"; - if (input.type === "session.prompt") - emit({ - type: "session.prompt_result", - sessionId: input.sessionId, - clientMessageId: input.prompt.clientMessageId, - result: { type: "failed", error: { message, code } }, - }); - else if ("requestId" in input) - emit({ type: "request.failed", requestId: input.requestId, error: { message, code } }); - } - return { - version: 1, - capabilities: negotiated, - async send(input) { - if (closed) throw new AntigravityError("Antigravity connection is closed"); - requireProviderCapabilities(negotiated, input); - pending = pending - .then(async () => { - if (!closed) await dispatch(input); - return undefined; - }) - .catch((error) => failed(input, error)); - }, - onEvent(listener) { - listeners.add(listener); - return () => listeners.delete(listener); - }, - async close() { - if (closed) return; - closed = true; - await pending; - await Promise.all([...sessions.values()].map((opened) => opened.close())); - sessions.clear(); - listeners.clear(); - }, - }; -} diff --git a/plugins/antigravity-provider/test/agy.mjs b/plugins/antigravity-provider/test/agy.mjs deleted file mode 100644 index 91b0531b56..0000000000 --- a/plugins/antigravity-provider/test/agy.mjs +++ /dev/null @@ -1,102 +0,0 @@ -import { readFileSync, appendFileSync } from "node:fs"; -import { createInterface } from "node:readline"; - -const args = process.argv.slice(2); -const log = process.env.AGY_TEST_LOG; -function record(value) { - if (log) appendFileSync(log, `${JSON.stringify(value)}\n`); -} -record({ args, pid: process.pid, env: process.env.AGY_SESSION_VALUE }); -function fixture(name, stream = "stdout") { - return readFileSync(new URL(`./fixtures/${name}.${stream}.ndjson`, import.meta.url), "utf8") - .trim() - .split("\n") - .filter(Boolean) - .map((line) => JSON.parse(line).data); -} -if (args.includes("--version")) { - process.stdout.write(`${process.env.AGY_TEST_VERSION || "1.2.13"}\n`); - process.exit(0); -} -if (args.includes("models")) { - if (process.env.AGY_TEST_AUTH === "missing") { - process.stderr.write(fixture("models-unauthenticated", "stderr").join("\n")); - process.exit(1); - } - process.stdout.write(fixture("models-api-key").join("\n") + "\n"); - process.exit(0); -} -if (process.env.AGY_TEST_STARTUP === "auth") { - process.stderr.write(fixture("not-signed-in", "stderr").join("\n")); - process.stdout.write(JSON.stringify(fixture("not-signed-in")[0]) + "\n"); - process.exit(1); -} -if (process.env.AGY_TEST_STARTUP === "exit") process.exit(3); -if (process.env.AGY_TEST_STARTUP === "stderr") { - process.stderr.write("Error: authentication required. Run 'agy' to log in, then retry.\n"); - process.exit(1); -} -const resumeIndex = args.indexOf("--conversation"); -const conversationId = - resumeIndex >= 0 ? args[resumeIndex + 1] : fixture("text-turn")[0].conversation_id; -let denialResults = 0; -function emit(frame) { - const converted = structuredClone(frame); - if (converted.conversation_id !== undefined) converted.conversation_id = conversationId; - if (converted.step_update) converted.step_update.conversation_id = conversationId; - if (converted.result) { - converted.result.conversation_id = conversationId; - if (process.env.AGY_TEST_DENIAL_COUNTS && converted.result.denied_actions?.length) { - converted.result.denied_actions = Array.from( - { length: Number(process.env.AGY_TEST_DENIAL_COUNTS.split(",")[denialResults++]) }, - () => converted.result.denied_actions[0], - ); - } - } - process.stdout.write(JSON.stringify(converted) + "\n"); -} -const init = fixture("text-turn")[0]; -if (args.includes("--model")) init.init.model = args[args.indexOf("--model") + 1]; -emit(init); -let interrupted = null; -const turns = new Map(); -const lines = createInterface({ input: process.stdin }); -lines.on("line", (line) => { - const input = JSON.parse(line); - record({ input }); - const text = input.message.content; - let name = "text-turn"; - if (text.includes("Attached image: ")) name = "image-file-external"; - if (text.includes("TOOLS")) name = "tools-default"; - if (text.includes("DENIAL_REPLAY")) name = "denial-replay"; - if (text.includes("SUBAGENT")) name = "subagent"; - if (text.includes("MULTI")) name = "multi-turn-reasoning"; - if (text.includes("RESUME")) name = "resume"; - if (text.includes("HANG")) name = "sigint-mid-text"; - if (text.includes("TOOL_HANG")) name = "sigint-mid-tool"; - const frames = fixture(name).slice(1); - if (name.startsWith("sigint")) { - interrupted = frames[frames.length - 1]; - for (const frame of frames.slice(0, -1)) emit(frame); - return; - } - const count = frames.filter((frame) => frame.event === "result").length; - const ordinal = (turns.get(name) || 0) % count; - turns.set(name, ordinal + 1); - let resultCount = 0; - for (const frame of frames) { - if (resultCount === ordinal) emit(frame); - if (frame.event === "result") resultCount += 1; - } -}); -process.on("SIGINT", () => { - record({ signal: "SIGINT" }); - if (process.env.AGY_TEST_IGNORE_SIGNALS === "yes") return; - if (interrupted) emit(interrupted); - process.exit(1); -}); -process.on("SIGTERM", () => { - record({ signal: "SIGTERM" }); - if (process.env.AGY_TEST_IGNORE_SIGNALS !== "yes") process.exit(1); -}); -lines.on("close", () => process.exit(0)); diff --git a/plugins/antigravity-provider/test/fixtures/README.md b/plugins/antigravity-provider/test/fixtures/README.md deleted file mode 100644 index b4b1ef264f..0000000000 --- a/plugins/antigravity-provider/test/fixtures/README.md +++ /dev/null @@ -1,100 +0,0 @@ -# agy 1.2.13 stream-json captures - -`denial-replay.stdout.ndjson` replays two turns from one real 1.2.13 process: a denied shell -command, then a text-only answer whose result retains the first denial. The replay clock is -synthetic; frames retain the native cumulative denial payload. - -Captured on Linux x86_64 on 2026-09-29. These are observations from the supplied binary, not guarantees for the Google sign-in route. No product code or capture drivers are included. - -Each row names a capture prefix. Its `.stdin.ndjson`, `.stdout.ndjson`, `.stderr.ndjson`, and `.capture.ndjson` companions keep the streams separate; empty streams are empty files. Every line wraps the exact decoded JSON frame or text line in `{t_ms, stream, data}`. `t_ms` is milliseconds since construction immediately before spawn. Capture annotations record launch arguments, configuration, signals, EOF, timeouts, and exit code; they are not CLI output. Line references refer to the indicated file, starting at 1. - -Commands below run from ``. `` expands to `/.local/opt/agy/1.2.13/agy` in the real installation home; the child uses a separate isolated `` for state. All captures remove inherited credential/provider environment variables, set `HOME`, `XDG_CONFIG_HOME=/.config`, `XDG_CACHE_HOME=/.cache`, `XDG_DATA_HOME=/.local/share`, `BROWSER=/bin/true`, and loopback-only `NO_PROXY`/`no_proxy`. Neither PATH nor shell rc files changed. Resume rows reuse the original isolated HOME. - -For rows marked **API**, the environment also contains `GEMINI_API_KEY=` and `GOOGLE_GEMINI_BASE_URL=http://127.0.0.1:`, and `/.gemini/antigravity-cli/settings.json` contains `{"modelProvider":"gemini"}`. **Env only** sets those two variables without the setting; **None** has neither. The loopback translator reused the prior QA route and forwarded real requests to a paid upstream model. Its credential was read programmatically inside the temporary proxy, with later captures passing it through the proxy environment. It was never passed to agy or recorded. The translator maps all requested Gemini models to one upstream Gemini model: request routing and CLI selection are verified, native model identity/quality is not. - -The translator emits text, function calls, thought parts and usage. Its usage mapping does not populate Gemini thinking/cache counts, so observed zero `thinking_tokens`/`cache_read_tokens` does not prove native zeros. No dedicated reasoning frames appeared, including the matched high-effort probe. - -``, ``, ``, ``, ``, and `` are redactions. Unrelated project/model names and help examples are redacted without removing frames. Delta chunk boundaries remain; a path split across deltas can have a different placeholder spelling than the complete result. UUIDs are retained for conversation correlation. - -| Fixture prefix | Exact argv (redacted) | Auth | Demonstrates | agy | -| --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | -| `agents` | ` agents` | API | Available agents listing is plain text. | 1.2.13 | -| `api-key-env-only` | ` --input-format stream-json --output-format stream-json` | Env only | Dummy key and custom endpoint alone do not select API-key auth. | 1.2.13 | -| `api-key-settings` | ` --input-format stream-json --output-format stream-json` | API | modelProvider=gemini yields init; missing event input is rejected. | 1.2.13 | -| `cwd-without-add-dir` | ` --input-format stream-json --output-format stream-json --dangerously-skip-permissions` | API | Without add-dir, model-issued pwd returned launch cwd; required tool Cwd is not exposed by this wire frame. | 1.2.13 | -| `effort-unsupported-family` | ` --input-format stream-json --output-format stream-json --model '' --effort high` | API | Unsupported non-Gemini family rejects effort; model reference redacted. | 1.2.13 | -| `effort-encoded-match` | ` --input-format stream-json --output-format stream-json --model gemini-3.8-flash-high --effort high` | API | Matching high effort accepted; arithmetic turn; no thought delta observed. | 1.2.13 | -| `effort-encoded-mismatch` | ` --input-format stream-json --output-format stream-json --model gemini-3.8-flash-high --effort low` | API | High-suffixed model conflicts with effort=low; no init. | 1.2.13 | -| `effort-mismatch` | ` --input-format stream-json --output-format stream-json --model gemini-3.1-pro-preview --effort low` | API | Unlisted preview ID does not support effort; startup ERROR. | 1.2.13 | -| `help` | ` --help` | None | Launch flags and subcommands; help is on stderr. | 1.2.13 | -| `image-content` | ` --input-format stream-json --output-format stream-json --disable-slash-commands` | API | Image source block rejected: only text blocks supported. | 1.2.13 | -| `mcp-add-http` | ` mcp add phase0 'http://127.0.0.1:/mcp'` | None | Writes .gemini/config/mcp_config.json; file content recorded in capture annotations. | 1.2.13 | -| `mcp-add` | ` mcp add phase0 --url 'http://127.0.0.1:/mcp'` | None | Invalid --url syntax rejected; no configuration written. | 1.2.13 | -| `mcp-global` | ` --input-format stream-json --output-format stream-json --add-dir '' --disable-slash-commands --dangerously-skip-permissions` | API | Global HTTP server discovered; strict tool call fails because arguments is a string. | 1.2.13 | -| `mcp-help` | ` mcp add --help` | None | MCP add syntax, HTTP transport, headers/env; flags precede name. | 1.2.13 | -| `mcp-list` | ` mcp list` | None | Model-free empty server list, exit 0. | 1.2.13 | -| `mcp-project-config` | ` --input-format stream-json --output-format stream-json --add-dir '' --disable-slash-commands --dangerously-skip-permissions` | API | Workspace .gemini/config.json mcpServers not discovered in this probe. | 1.2.13 | -| `mcp-project-isolated` | ` --input-format stream-json --output-format stream-json --add-dir '' --dangerously-skip-permissions` | API | Project MCP discovery; no global MCP config before/after; strict arguments error. | 1.2.13 | -| `mcp-project` | ` --input-format stream-json --output-format stream-json --add-dir '' --disable-slash-commands --dangerously-skip-permissions` | API | Project .agents/mcp_config.json discovers HTTP server; strict call rejects string arguments; later translator 502 retained. | 1.2.13 | -| `mcp-second-add-dir` | ` --input-format stream-json --output-format stream-json --disable-slash-commands --dangerously-skip-permissions --add-dir '' --add-dir '/owned'` | API | Discovers project MCP from the owned directory; strict arguments fail. Shell uses owned directory and writes occur in both directories. | 1.2.13 | -| `mcp-owned-first-add-dir` | ` --input-format stream-json --output-format stream-json --disable-slash-commands --dangerously-skip-permissions --add-dir '/owned' --add-dir ''` | API | Reversing argv does not restore command working directory; owned directory remains the shell default. | 1.2.13 | -| `mcp-owned-cwd` | ` --input-format stream-json --output-format stream-json --disable-slash-commands --dangerously-skip-permissions --add-dir ''`, launched in `/owned` through a symlink | API | Discovers owned project MCP, but shell and initial read resolve against owned cwd; writes occur in both directories. | 1.2.13 | -| `mcp-agent-file` | ` --input-format stream-json --output-format stream-json --disable-slash-commands --dangerously-skip-permissions --add-dir '' --agent '/owned/agent.md'` | API | Direct external agent filename falls back to default; no MCP discovery. Shell/read/write use workspace. | 1.2.13 | -| `mcp-agent-definition` | ` --input-format stream-json --output-format stream-json --add-dir '/owned' --add-dir '' --agent phase2 --dangerously-skip-permissions` | API | Nested Markdown definition with inline mcpServers is not found; default agent, no MCP server. | 1.2.13 | -| `mcp-agent-discovered` | ` --input-format stream-json --output-format stream-json --add-dir '/owned' --add-dir '' --agent phase2 --dangerously-skip-permissions` | API | Flat Markdown definition plus agents.json manifest is also not found; default agent, no MCP server. | 1.2.13 | -| `image-file-external` | ` --input-format stream-json --output-format stream-json --add-dir '' --disable-slash-commands` | API | Default mode reads an external image path with view_file and correctly describes its pixels, without an extra add-dir. | 1.2.13 | -| `model-switch-invalid` | ` --input-format stream-json --output-format stream-json --add-dir '' --conversation b966b16a-caf4-448a-bb24-0e34956022b0 --model gemini-3.1-flash-lite-preview` | API | Unlisted model ID rejected before init. | 1.2.13 | -| `model-switch-network` | ` --input-format stream-json --output-format stream-json --conversation b966b16a-caf4-448a-bb24-0e34956022b0 --model gemini-3.8-flash-low` | API | Listed model respawn plus actual Gemini request path in capture annotations; COBALT preserved. | 1.2.13 | -| `model-switch-resume` | ` --input-format stream-json --output-format stream-json --add-dir '' --conversation b966b16a-caf4-448a-bb24-0e34956022b0 --model gemini-3.1-flash-high` | API | Another unlisted model ID rejected before init. | 1.2.13 | -| `model-switch-valid` | ` --input-format stream-json --output-format stream-json --conversation b966b16a-caf4-448a-bb24-0e34956022b0 --model gemini-3.8-flash-low` | API | Listed low-effort model accepted on same conversation; COBALT preserved. | 1.2.13 | -| `models-api-key` | ` models` | API | Authenticated catalog is tab-separated model ID and display label. | 1.2.13 | -| `models-unauthenticated` | ` models` | None | Models requires auth; no catalog on stdout. | 1.2.13 | -| `multi-turn-reasoning` | ` --input-format stream-json --output-format stream-json --add-dir '' --disable-slash-commands` | API | Two text turns with arithmetic; per-generation and cumulative usage. No thought delta observed. | 1.2.13 | -| `not-signed-in` | ` --input-format stream-json --output-format stream-json` | None | No credentials: result ERROR, no init, exit 1. | 1.2.13 | -| `project-skill` | ` --input-format stream-json --output-format stream-json --add-dir '' --disable-slash-commands` | API | Project skill read via view_file; marker reply. No distinct skill event. | 1.2.13 | -| `question-auto-denial` | ` --input-format stream-json --output-format stream-json --add-dir ''` | API | Requested ask_question becomes unknown DONE step, auto-skipped; no question payload on wire. | 1.2.13 | -| `resume-after-interrupt` | ` --input-format stream-json --output-format stream-json --conversation a2c2132f-0f10-400b-a142-74f82230f449` | API | Same interrupted conversation resumes and accepts another turn. | 1.2.13 | -| `resume` | ` --input-format stream-json --output-format stream-json --add-dir '' --disable-slash-commands --conversation b966b16a-caf4-448a-bb24-0e34956022b0` | API | Remembered COBALT after respawn; no old steps replayed. | 1.2.13 | -| `schema-prompt` | ` --input-format stream-json --output-format stream-json` | API | Unknown prompt event ignored with stderr warning; no turn. | 1.2.13 | -| `schema-user-message` | ` --input-format stream-json --output-format stream-json` | API | Decoding a string message fails before event dispatch. | 1.2.13 | -| `schema-user` | ` --input-format stream-json --output-format stream-json` | API | String message rejected; message must be an object. | 1.2.13 | -| `sigint-mid-text` | ` --input-format stream-json --output-format stream-json --add-dir '' --disable-slash-commands --dangerously-skip-permissions` | API | SIGINT after first text delta: interrupted result and exit 1. | 1.2.13 | -| `sigint-mid-tool` | ` --input-format stream-json --output-format stream-json --add-dir '' --dangerously-skip-permissions` | API | SIGINT while run_command ACTIVE: interrupted result and exit 1. | 1.2.13 | -| `sigterm` | ` --input-format stream-json --output-format stream-json --add-dir '' --disable-slash-commands --dangerously-skip-permissions` | API | SIGTERM after user_input: interrupted result and exit 1. | 1.2.13 | -| `slash-disabled` | ` --input-format stream-json --output-format stream-json --disable-slash-commands` | API | /help sent literally to model; normal turn with built-in skill reads. | 1.2.13 | -| `slash-enabled` | ` --input-format stream-json --output-format stream-json` | API | /help rejected in driver mode; result ERROR and exit 2. | 1.2.13 | -| `stdin-close-mid-turn` | ` --input-format stream-json --output-format stream-json --add-dir '' --disable-slash-commands --dangerously-skip-permissions` | API | EOF after user_input: active turn finishes successfully, exit 0. | 1.2.13 | -| `stdin-mid-turn` | ` --input-format stream-json --output-format stream-json --add-dir '' --disable-slash-commands --dangerously-skip-permissions` | API | Second prompt after first user_input queues a separate turn; two results. | 1.2.13 | -| `subagent` | ` --input-format stream-json --output-format stream-json --add-dir '' --dangerously-skip-permissions` | API | Define/invoke child; subagent_info contains child ID/log URI/workspaces; child reply reaches parent. | 1.2.13 | -| `text-block` | ` --input-format stream-json --output-format stream-json --add-dir ''` | API | Array of text content blocks accepted. | 1.2.13 | -| `text-turn` | ` --input-format stream-json --output-format stream-json --add-dir ''` | API | event=user, message.content string; streaming text and final usage. | 1.2.13 | -| `tools-accept-edits` | ` --input-format stream-json --output-format stream-json --add-dir '' --disable-slash-commands --mode accept-edits` | API | Read, create, edit allowed; shell auto-denied. Result SUCCESS includes denied_actions. No permission request. | 1.2.13 | -| `tools-default` | ` --input-format stream-json --output-format stream-json --add-dir '' --disable-slash-commands` | API | Read, create, edit allowed; shell auto-denied. Result SUCCESS includes denied_actions. No permission request. | 1.2.13 | -| `tools-plan` | ` --input-format stream-json --output-format stream-json --add-dir '' --disable-slash-commands --mode plan` | API | Read, create, edit allowed; shell auto-denied. Result SUCCESS includes denied_actions. No permission request. | 1.2.13 | -| `tools-sandbox` | ` --input-format stream-json --output-format stream-json --add-dir '' --disable-slash-commands --sandbox` | API | Read, create, edit allowed; shell auto-denied. Result SUCCESS includes denied_actions. No permission request. | 1.2.13 | -| `tools-skip-permissions` | ` --input-format stream-json --output-format stream-json --add-dir '' --disable-slash-commands --dangerously-skip-permissions` | API | Read, create, edit and shell succeed; permission_mode=always-proceed. | 1.2.13 | -| `version` | ` --version` | None | Installed binary reports 1.2.13. | 1.2.13 | -| `mcp-http.requests.ndjson` | `node /mcp-server.mjs` alongside the MCP argv above | None | Actual HTTP JSON-RPC requests to a strict SDK server; `params.arguments` is a string. Server implements initialize, tools/list, and echo. | 1.2.13 client | -| `state-inspection.ndjson` | `HOME= strace -f -e trace=file -o /files.trace --input-format stream-json --output-format stream-json`; `python3 /finalize.py`; `firecrawl scrape --format markdown` | None | Binary checksum, isolated state accesses, read-only SQLite listings, written file contents, and manifest retrieval caveat. Inspection data, not agy stdout. | 1.2.13 | - -`project-skill` used `/.agents/skills/phase0-marker/SKILL.md` with name `phase0-marker`, description “Return the Phase 0 marker when requested,” and instructions “Respond with PHASE0_SKILL_MARKER. No tools are needed after reading this skill.” - -Each tools capture begins with `input.txt` containing `PHASE0_READ_MARKER`, then requests create→replace from `PHASE0_WRITE` to `PHASE0_EDIT`, then `printf PHASE0_SHELL`. `state-inspection.ndjson` records the resulting files. These successful writes disprove an enforced read-only boundary for default/plan on this credential route. `--sandbox` alone still denies the shell at permission policy, so OS sandbox enforcement is unverified. - -## MCP discovery - -Phase 0 project setup (`mcp-project` and `mcp-project-isolated`) places `{"mcpServers":{"phase0":{"serverUrl":"http://127.0.0.1:/mcp"}}}` in `/.agents/mcp_config.json`. `mcp-project-config` instead uses `/.gemini/config.json`; that file did not expose the server. HTTP calls send string arguments and fail strict validation. - -Phase 2 places the same HTTP configuration, named `phase2`, in a plugin-owned `/owned/.agents/mcp_config.json`. Both add-dir orders discover it, as does launching in the owned directory and adding the real workspace. All three turns execute shell `pwd` in the owned directory. The file turns read the workspace marker but also write `output.txt` into the owned directory. These routes fail the requirement that tools resolve against the user's workspace. Reordering arguments did not change the observed shell cwd; the precise directory selection rule is unverified. - -The external `--agent` file, nested agent definition, and flat definition with an `agents.json` manifest contain inline `mcpServers`. Native logs record “not found, falling back to default” for each. These experiments do not establish that every agent definition format is unsupported. `parity-inspection.ndjson` records their definitions and log evidence. Searching the tested binary's strings found no `AGY_MCP`, `MCP_CONFIG`, `AGY_CONFIG`, or `ANTIGRAVITY_CONFIG` override; no environment discovery route was verified. - -No tested MCP route satisfies both discovery and workspace ownership. MCP implementation stopped at this gate: no relay or MCP capability change was added. Images and the daemon e2e continued independently. No global MCP configuration exists in the isolated homes, and no configuration was written into the user repository. - -## Image file fallback - -`image-file-external.png` contains a red triangle on the left and a blue circle on the lower right, against white. The prompt supplies only its path and asks for visible shapes and colors. `image-file-external.stdout.ndjson:4–5` records a successful `view_file`; the result describes the pixels correctly. The native function response supplies bytes at `functionResponse.parts[].inlineData`. The temporary translator was corrected to forward these nested parts to the model; forwarding top-level parts alone dropped the image. `parity-inspection.ndjson` records the image hash and response-part location. - -This verifies one default-mode CLI turn reading a file outside the workspace without an extra add-dir. A later isolated-daemon run attached the same image through the provider and returned the same shapes and colors after a successful `view_file`. The provider advertises `prompt.image`; MCP remains outside the parity implementation. - -Unverified: Google login behaviour; native reasoning/cached-token accounting; filesystem/network isolation under sandbox plus approved commands; exhaustive tool policy; other agent definition formats or environment MCP overrides; child transcript streaming beyond parent metadata; a stable public conversation-list command. No fresh login was created. The fetched official manifest returned 1.2.12 while this binary reports 1.2.13; the checksum identifies the tested binary, but that response does not establish its release provenance. diff --git a/plugins/antigravity-provider/test/fixtures/agents.capture.ndjson b/plugins/antigravity-provider/test/fixtures/agents.capture.ndjson deleted file mode 100644 index cc01e91070..0000000000 --- a/plugins/antigravity-provider/test/fixtures/agents.capture.ndjson +++ /dev/null @@ -1,2 +0,0 @@ -{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "agents"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} -{"t_ms": 309, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/agents.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/agents.stderr.ndjson deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/plugins/antigravity-provider/test/fixtures/agents.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/agents.stdin.ndjson deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/plugins/antigravity-provider/test/fixtures/agents.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/agents.stdout.ndjson deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/plugins/antigravity-provider/test/fixtures/api-key-env-only.capture.ndjson b/plugins/antigravity-provider/test/fixtures/api-key-env-only.capture.ndjson deleted file mode 100644 index 9c7a770416..0000000000 --- a/plugins/antigravity-provider/test/fixtures/api-key-env-only.capture.ndjson +++ /dev/null @@ -1,2 +0,0 @@ -{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json"], "cwd": "", "home": "", "settings": null, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} -{"t_ms": 452, "stream": "capture", "data": {"exit_code": 1}} diff --git a/plugins/antigravity-provider/test/fixtures/api-key-env-only.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/api-key-env-only.stderr.ndjson deleted file mode 100644 index d315088707..0000000000 --- a/plugins/antigravity-provider/test/fixtures/api-key-env-only.stderr.ndjson +++ /dev/null @@ -1,2 +0,0 @@ -{"t_ms": 288, "stream": "stderr", "data": "Error: authentication required. Run 'agy' to log in, then retry."} -{"t_ms": 288, "stream": "stderr", "data": "error: authentication failed or timed out"} diff --git a/plugins/antigravity-provider/test/fixtures/api-key-env-only.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/api-key-env-only.stdin.ndjson deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/plugins/antigravity-provider/test/fixtures/api-key-env-only.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/api-key-env-only.stdout.ndjson deleted file mode 100644 index 60cc38245f..0000000000 --- a/plugins/antigravity-provider/test/fixtures/api-key-env-only.stdout.ndjson +++ /dev/null @@ -1 +0,0 @@ -{"t_ms": 288, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "", "status": "ERROR", "response": "", "error": "authentication failed or timed out", "duration_seconds": 0, "num_turns": 0, "usage": {"input_tokens": 0, "output_tokens": 0, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 0}}}} diff --git a/plugins/antigravity-provider/test/fixtures/api-key-settings.capture.ndjson b/plugins/antigravity-provider/test/fixtures/api-key-settings.capture.ndjson deleted file mode 100644 index 1c03483e72..0000000000 --- a/plugins/antigravity-provider/test/fixtures/api-key-settings.capture.ndjson +++ /dev/null @@ -1,2 +0,0 @@ -{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} -{"t_ms": 525, "stream": "capture", "data": {"exit_code": 1}} diff --git a/plugins/antigravity-provider/test/fixtures/api-key-settings.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/api-key-settings.stderr.ndjson deleted file mode 100644 index 1c47437a40..0000000000 --- a/plugins/antigravity-provider/test/fixtures/api-key-settings.stderr.ndjson +++ /dev/null @@ -1 +0,0 @@ -{"t_ms": 374, "stream": "stderr", "data": "error: stream input message is missing the \"event\" field"} diff --git a/plugins/antigravity-provider/test/fixtures/api-key-settings.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/api-key-settings.stdin.ndjson deleted file mode 100644 index 97362f53ed..0000000000 --- a/plugins/antigravity-provider/test/fixtures/api-key-settings.stdin.ndjson +++ /dev/null @@ -1 +0,0 @@ -{"t_ms": 373, "stream": "stdin", "data": {"type": "user", "message": {"role": "user", "content": "Reply with PHASE0_HELLO. No tools."}}} diff --git a/plugins/antigravity-provider/test/fixtures/api-key-settings.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/api-key-settings.stdout.ndjson deleted file mode 100644 index 567b1fc1f1..0000000000 --- a/plugins/antigravity-provider/test/fixtures/api-key-settings.stdout.ndjson +++ /dev/null @@ -1,2 +0,0 @@ -{"t_ms": 373, "stream": "stdout", "data": {"event": "init", "conversation_id": "e629b41a-decf-4212-a9fb-3809e8f21122", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "request-review"}}} -{"t_ms": 374, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "e629b41a-decf-4212-a9fb-3809e8f21122", "status": "ERROR", "response": "", "error": "stream input message is missing the \"event\" field", "duration_seconds": 0, "num_turns": 0, "usage": {"input_tokens": 0, "output_tokens": 0, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 0}}}} diff --git a/plugins/antigravity-provider/test/fixtures/cwd-without-add-dir.capture.ndjson b/plugins/antigravity-provider/test/fixtures/cwd-without-add-dir.capture.ndjson deleted file mode 100644 index 78eedeec6f..0000000000 --- a/plugins/antigravity-provider/test/fixtures/cwd-without-add-dir.capture.ndjson +++ /dev/null @@ -1,2 +0,0 @@ -{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--dangerously-skip-permissions"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} -{"t_ms": 5003, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/cwd-without-add-dir.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/cwd-without-add-dir.stderr.ndjson deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/plugins/antigravity-provider/test/fixtures/cwd-without-add-dir.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/cwd-without-add-dir.stdin.ndjson deleted file mode 100644 index 52514739aa..0000000000 --- a/plugins/antigravity-provider/test/fixtures/cwd-without-add-dir.stdin.ndjson +++ /dev/null @@ -1 +0,0 @@ -{"t_ms": 318, "stream": "stdin", "data": {"event": "user", "message": {"content": "Use run_command to run pwd, WaitMsBeforeAsync 1000. Report the output."}}} diff --git a/plugins/antigravity-provider/test/fixtures/cwd-without-add-dir.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/cwd-without-add-dir.stdout.ndjson deleted file mode 100644 index 469cdadcca..0000000000 --- a/plugins/antigravity-provider/test/fixtures/cwd-without-add-dir.stdout.ndjson +++ /dev/null @@ -1,11 +0,0 @@ -{"t_ms": 318, "stream": "stdout", "data": {"event": "init", "conversation_id": "3cea6151-badf-4b4a-ad58-bf3c0b02db25", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "always-proceed"}}} -{"t_ms": 347, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "3cea6151-badf-4b4a-ad58-bf3c0b02db25", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} -{"t_ms": 3066, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "3cea6151-badf-4b4a-ad58-bf3c0b02db25", "step_index": 1, "state": "DONE", "step_type": "agent_response", "duration_seconds": 2.698205516, "usage": {"input_tokens": 11255, "output_tokens": 138, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11393}}}} -{"t_ms": 3066, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "3cea6151-badf-4b4a-ad58-bf3c0b02db25", "step_index": 2, "state": "ACTIVE", "step_type": "tool", "tool_name": "run_command", "tool_info": {"name": "run_command", "parameters": {"CommandLine": "pwd"}}}}} -{"t_ms": 3066, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "3cea6151-badf-4b4a-ad58-bf3c0b02db25", "step_index": 2, "state": "DONE", "step_type": "tool", "tool_name": "run_command", "duration_seconds": 0.006078385, "tool_info": {"name": "run_command", "parameters": {"CommandLine": "pwd"}, "output": "\r\n"}}}} -{"t_ms": 4626, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "3cea6151-badf-4b4a-ad58-bf3c0b02db25", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "The output"}}} -{"t_ms": 4678, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "3cea6151-badf-4b4a-ad58-bf3c0b02db25", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " of running "}}} -{"t_ms": 4704, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "3cea6151-badf-4b4a-ad58-bf3c0b02db25", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "`pwd` is:\n\n```text\n/works"}}} -{"t_ms": 4729, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "3cea6151-badf-4b4a-ad58-bf3c0b02db25", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "pace\n```"}}} -{"t_ms": 4881, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "3cea6151-badf-4b4a-ad58-bf3c0b02db25", "step_index": 3, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 1.824928838, "usage": {"input_tokens": 11382, "output_tokens": 24, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11406}}}} -{"t_ms": 4881, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "3cea6151-badf-4b4a-ad58-bf3c0b02db25", "status": "SUCCESS", "response": "The output of running `pwd` is:\n\n```text\n\n```\n", "duration_seconds": 4.555206291, "num_turns": 1, "usage": {"input_tokens": 22637, "output_tokens": 162, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 22799}}}} diff --git a/plugins/antigravity-provider/test/fixtures/denial-replay.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/denial-replay.stdout.ndjson deleted file mode 100644 index b667b505cc..0000000000 --- a/plugins/antigravity-provider/test/fixtures/denial-replay.stdout.ndjson +++ /dev/null @@ -1,11 +0,0 @@ -{"t_ms":0,"stream":"stdout","data":{"event":"init","conversation_id":"e8243164-64d5-4419-9dbb-c0733300b557","init":{"cwd":"/tmp/agy-p3/workspace","tools":["ask_custom_permission","ask_permission","ask_question","browser_click_element","browser_drag_pixel_to_pixel","browser_get_dom","browser_get_network_request","browser_input","browser_list_network_requests","browser_mouse_down","browser_mouse_up","browser_move_mouse","browser_press_key","browser_refresh_page","browser_resize_window","browser_scroll","browser_scroll_dom","browser_select_option","browser_subagent","call_mcp_tool","capture_browser_console_logs","capture_browser_screenshot","click_browser_pixel","command_status","define_subagent","delete_knowledge","execute_browser_javascript","find_by_name","finish","generate_image","grep_search","invoke_subagent","list_browser_pages","list_dir","list_permissions","list_resources","manage_inbox","manage_subagents","manage_task","multi_replace_file_content","notebook_edit","notebook_execution","open_browser_url","read_browser_page","read_resource","read_url_content","replace_file_content","run_command","schedule","search_web","sed_file","send_command_input","send_message","view_file","wait","wait_5_seconds","write_to_file"],"permission_mode":"request-review"}}} -{"t_ms":1,"stream":"stdout","data":{"event":"step_update","step_update":{"conversation_id":"e8243164-64d5-4419-9dbb-c0733300b557","step_index":0,"state":"DONE","step_type":"user_input"}}} -{"t_ms":2,"stream":"stdout","data":{"event":"step_update","step_update":{"conversation_id":"e8243164-64d5-4419-9dbb-c0733300b557","step_index":1,"state":"DONE","step_type":"agent_response","duration_seconds":2.86246236,"usage":{"input_tokens":11252,"output_tokens":163,"thinking_tokens":0,"cache_read_tokens":0,"total_tokens":11415}}}} -{"t_ms":3,"stream":"stdout","data":{"event":"step_update","step_update":{"conversation_id":"e8243164-64d5-4419-9dbb-c0733300b557","step_index":2,"state":"ACTIVE","step_type":"tool","tool_name":"run_command","tool_info":{"name":"run_command","parameters":{"CommandLine":"printf AGY_DENIAL_PROBE"}}}}} -{"t_ms":4,"stream":"stdout","data":{"event":"step_update","step_update":{"conversation_id":"e8243164-64d5-4419-9dbb-c0733300b557","step_index":2,"state":"ERROR","step_type":"tool","tool_name":"run_command","duration_seconds":0.002264915,"tool_info":{"name":"run_command","parameters":{"CommandLine":"printf AGY_DENIAL_PROBE"},"error":{"type":"TOOL_ERROR","message":"permission check failed for command \"printf AGY_DENIAL_PROBE\": user denied permission to run command:\nprintf AGY_DENIAL_PROBE\nDo not attempt to circumvent this denial by rephrasing the command, using alternative tools/scripts (e.g. python, sh, curl), or accessing the same target resource. Proceed without performing this action."}}}}} -{"t_ms":5,"stream":"stdout","data":{"event":"result","result":{"conversation_id":"e8243164-64d5-4419-9dbb-c0733300b557","status":"SUCCESS","response":"","duration_seconds":2.890969963,"num_turns":1,"usage":{"input_tokens":11252,"output_tokens":163,"thinking_tokens":0,"cache_read_tokens":0,"total_tokens":11415},"denied_actions":[{"action":"command","display_name":"RunCommand"}]}}} -{"t_ms":6,"stream":"stdout","data":{"event":"step_update","step_update":{"conversation_id":"e8243164-64d5-4419-9dbb-c0733300b557","step_index":3,"state":"DONE","step_type":"user_input"}}} -{"t_ms":7,"stream":"stdout","data":{"event":"step_update","step_update":{"conversation_id":"e8243164-64d5-4419-9dbb-c0733300b557","step_index":4,"state":"ACTIVE","step_type":"agent_response","text_delta":"FOLLOWUP"}}} -{"t_ms":8,"stream":"stdout","data":{"event":"step_update","step_update":{"conversation_id":"e8243164-64d5-4419-9dbb-c0733300b557","step_index":4,"state":"ACTIVE","step_type":"agent_response","text_delta":"_OK"}}} -{"t_ms":9,"stream":"stdout","data":{"event":"step_update","step_update":{"conversation_id":"e8243164-64d5-4419-9dbb-c0733300b557","step_index":4,"state":"DONE","step_type":"agent_response","text_delta":"\n","duration_seconds":2.11565723,"usage":{"input_tokens":11533,"output_tokens":4,"thinking_tokens":0,"cache_read_tokens":0,"total_tokens":11537}}}} -{"t_ms":10,"stream":"stdout","data":{"event":"result","result":{"conversation_id":"e8243164-64d5-4419-9dbb-c0733300b557","status":"SUCCESS","response":"FOLLOWUP_OK\n","duration_seconds":5.028454184,"num_turns":2,"usage":{"input_tokens":22785,"output_tokens":167,"thinking_tokens":0,"cache_read_tokens":0,"total_tokens":22952},"denied_actions":[{"action":"command","display_name":"RunCommand"}]}}} diff --git a/plugins/antigravity-provider/test/fixtures/effort-encoded-match.capture.ndjson b/plugins/antigravity-provider/test/fixtures/effort-encoded-match.capture.ndjson deleted file mode 100644 index 89227b65a3..0000000000 --- a/plugins/antigravity-provider/test/fixtures/effort-encoded-match.capture.ndjson +++ /dev/null @@ -1,2 +0,0 @@ -{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--model", "gemini-3.8-flash-high", "--effort", "high"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} -{"t_ms": 4787, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/effort-encoded-match.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/effort-encoded-match.stderr.ndjson deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/plugins/antigravity-provider/test/fixtures/effort-encoded-match.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/effort-encoded-match.stdin.ndjson deleted file mode 100644 index 817c98ad5b..0000000000 --- a/plugins/antigravity-provider/test/fixtures/effort-encoded-match.stdin.ndjson +++ /dev/null @@ -1 +0,0 @@ -{"t_ms": 296, "stream": "stdin", "data": {"event": "user", "message": {"content": "Solve 143*127. Think carefully before answering. No tools."}}} diff --git a/plugins/antigravity-provider/test/fixtures/effort-encoded-match.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/effort-encoded-match.stdout.ndjson deleted file mode 100644 index e56f554df9..0000000000 --- a/plugins/antigravity-provider/test/fixtures/effort-encoded-match.stdout.ndjson +++ /dev/null @@ -1,19 +0,0 @@ -{"t_ms": 295, "stream": "stdout", "data": {"event": "init", "conversation_id": "a090b266-2ea9-418c-bb02-c3267268b67b", "init": {"model": "gemini-3.8-flash-high", "cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "request-review"}}} -{"t_ms": 323, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a090b266-2ea9-418c-bb02-c3267268b67b", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} -{"t_ms": 3877, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a090b266-2ea9-418c-bb02-c3267268b67b", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "**18,161**\n\n### Step-by-"}}} -{"t_ms": 3903, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a090b266-2ea9-418c-bb02-c3267268b67b", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "Step Breakdo"}}} -{"t_ms": 3928, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a090b266-2ea9-418c-bb02-c3267268b67b", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "wn:\nUsing the difference"}}} -{"t_ms": 3953, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a090b266-2ea9-418c-bb02-c3267268b67b", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " of squares "}}} -{"t_ms": 3979, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a090b266-2ea9-418c-bb02-c3267268b67b", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "formula $("}}} -{"t_ms": 4055, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a090b266-2ea9-418c-bb02-c3267268b67b", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "a + b)(a - b) = a^2 - b^2$:\n- $143 ="}}} -{"t_ms": 4081, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a090b266-2ea9-418c-bb02-c3267268b67b", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " 135"}}} -{"t_ms": 4132, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a090b266-2ea9-418c-bb02-c3267268b67b", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " + 8$\n- $127"}}} -{"t_ms": 4157, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a090b266-2ea9-418c-bb02-c3267268b67b", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " = 135 - 8$\n\n$$143"}}} -{"t_ms": 4259, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a090b266-2ea9-418c-bb02-c3267268b67b", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " \\times 127 = 135^2 - 8^"}}} -{"t_ms": 4284, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a090b266-2ea9-418c-bb02-c3267268b67b", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "2$$\n$$135"}}} -{"t_ms": 4385, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a090b266-2ea9-418c-bb02-c3267268b67b", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "^2 = 18,225$$\n$$8^2 = 64"}}} -{"t_ms": 4411, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a090b266-2ea9-418c-bb02-c3267268b67b", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "$$\n$$18,22"}}} -{"t_ms": 4462, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a090b266-2ea9-418c-bb02-c3267268b67b", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "5 - 64 = \\ma"}}} -{"t_ms": 4487, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a090b266-2ea9-418c-bb02-c3267268b67b", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "thbf{18,161}$$"}}} -{"t_ms": 4640, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a090b266-2ea9-418c-bb02-c3267268b67b", "step_index": 1, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 4.300859666, "usage": {"input_tokens": 10982, "output_tokens": 553, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11535}}}} -{"t_ms": 4640, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "a090b266-2ea9-418c-bb02-c3267268b67b", "status": "SUCCESS", "response": "**18,161**\n\n### Step-by-Step Breakdown:\nUsing the difference of squares formula $(a + b)(a - b) = a^2 - b^2$:\n- $143 = 135 + 8$\n- $127 = 135 - 8$\n\n$$143 \\times 127 = 135^2 - 8^2$$\n$$135^2 = 18,225$$\n$$8^2 = 64$$\n$$18,225 - 64 = \\mathbf{18,161}$$\n", "duration_seconds": 4.326461265, "num_turns": 1, "usage": {"input_tokens": 10982, "output_tokens": 553, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11535}}}} diff --git a/plugins/antigravity-provider/test/fixtures/effort-encoded-mismatch.capture.ndjson b/plugins/antigravity-provider/test/fixtures/effort-encoded-mismatch.capture.ndjson deleted file mode 100644 index 22f8817b70..0000000000 --- a/plugins/antigravity-provider/test/fixtures/effort-encoded-mismatch.capture.ndjson +++ /dev/null @@ -1,3 +0,0 @@ -{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--model", "gemini-3.8-flash-high", "--effort", "low"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} -{"t_ms": 5006, "stream": "capture", "data": {"timeout_waiting_for": "init", "seconds": 5}} -{"t_ms": 5006, "stream": "capture", "data": {"exit_code": 1}} diff --git a/plugins/antigravity-provider/test/fixtures/effort-encoded-mismatch.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/effort-encoded-mismatch.stderr.ndjson deleted file mode 100644 index 1807a73321..0000000000 --- a/plugins/antigravity-provider/test/fixtures/effort-encoded-mismatch.stderr.ndjson +++ /dev/null @@ -1 +0,0 @@ -{"t_ms": 296, "stream": "stderr", "data": "error: invalid model selection (--model \"gemini-3.8-flash-high\" --effort \"low\"): --model gemini-3.8-flash-high conflicts with --effort=low"} diff --git a/plugins/antigravity-provider/test/fixtures/effort-encoded-mismatch.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/effort-encoded-mismatch.stdin.ndjson deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/plugins/antigravity-provider/test/fixtures/effort-encoded-mismatch.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/effort-encoded-mismatch.stdout.ndjson deleted file mode 100644 index b9669aaaf4..0000000000 --- a/plugins/antigravity-provider/test/fixtures/effort-encoded-mismatch.stdout.ndjson +++ /dev/null @@ -1 +0,0 @@ -{"t_ms": 296, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "", "status": "ERROR", "response": "", "error": "invalid model selection (--model \"gemini-3.8-flash-high\" --effort \"low\"): --model gemini-3.8-flash-high conflicts with --effort=low", "duration_seconds": 0, "num_turns": 0, "usage": {"input_tokens": 0, "output_tokens": 0, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 0}}}} diff --git a/plugins/antigravity-provider/test/fixtures/effort-mismatch.capture.ndjson b/plugins/antigravity-provider/test/fixtures/effort-mismatch.capture.ndjson deleted file mode 100644 index fbee5c5bc5..0000000000 --- a/plugins/antigravity-provider/test/fixtures/effort-mismatch.capture.ndjson +++ /dev/null @@ -1,2 +0,0 @@ -{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--model", "gemini-3.1-pro-preview", "--effort", "low"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} -{"t_ms": 415, "stream": "capture", "data": {"exit_code": 1}} diff --git a/plugins/antigravity-provider/test/fixtures/effort-mismatch.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/effort-mismatch.stderr.ndjson deleted file mode 100644 index 6072d28192..0000000000 --- a/plugins/antigravity-provider/test/fixtures/effort-mismatch.stderr.ndjson +++ /dev/null @@ -1 +0,0 @@ -{"t_ms": 298, "stream": "stderr", "data": "error: invalid model selection (--model \"gemini-3.1-pro-preview\" --effort \"low\"): --effort is not supported for model \"gemini-3.1-pro-preview\""} diff --git a/plugins/antigravity-provider/test/fixtures/effort-mismatch.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/effort-mismatch.stdin.ndjson deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/plugins/antigravity-provider/test/fixtures/effort-mismatch.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/effort-mismatch.stdout.ndjson deleted file mode 100644 index c0baa6d698..0000000000 --- a/plugins/antigravity-provider/test/fixtures/effort-mismatch.stdout.ndjson +++ /dev/null @@ -1 +0,0 @@ -{"t_ms": 298, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "", "status": "ERROR", "response": "", "error": "invalid model selection (--model \"gemini-3.1-pro-preview\" --effort \"low\"): --effort is not supported for model \"gemini-3.1-pro-preview\"", "duration_seconds": 0, "num_turns": 0, "usage": {"input_tokens": 0, "output_tokens": 0, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 0}}}} diff --git a/plugins/antigravity-provider/test/fixtures/effort-unsupported-family.capture.ndjson b/plugins/antigravity-provider/test/fixtures/effort-unsupported-family.capture.ndjson deleted file mode 100644 index 47eee607d9..0000000000 --- a/plugins/antigravity-provider/test/fixtures/effort-unsupported-family.capture.ndjson +++ /dev/null @@ -1,2 +0,0 @@ -{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--model", "", "--effort", "high"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} -{"t_ms": 358, "stream": "capture", "data": {"exit_code": 1}} diff --git a/plugins/antigravity-provider/test/fixtures/effort-unsupported-family.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/effort-unsupported-family.stderr.ndjson deleted file mode 100644 index f19c83a09b..0000000000 --- a/plugins/antigravity-provider/test/fixtures/effort-unsupported-family.stderr.ndjson +++ /dev/null @@ -1 +0,0 @@ -{"t_ms": 287, "stream": "stderr", "data": "error: invalid model selection (--model \"\" --effort \"high\"): --effort is not supported for model \"\""} diff --git a/plugins/antigravity-provider/test/fixtures/effort-unsupported-family.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/effort-unsupported-family.stdin.ndjson deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/plugins/antigravity-provider/test/fixtures/effort-unsupported-family.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/effort-unsupported-family.stdout.ndjson deleted file mode 100644 index b85109a9a9..0000000000 --- a/plugins/antigravity-provider/test/fixtures/effort-unsupported-family.stdout.ndjson +++ /dev/null @@ -1 +0,0 @@ -{"t_ms": 287, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "", "status": "ERROR", "response": "", "error": "invalid model selection (--model \"\" --effort \"high\"): --effort is not supported for model \"\"", "duration_seconds": 0, "num_turns": 0, "usage": {"input_tokens": 0, "output_tokens": 0, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 0}}}} diff --git a/plugins/antigravity-provider/test/fixtures/help.capture.ndjson b/plugins/antigravity-provider/test/fixtures/help.capture.ndjson deleted file mode 100644 index 6e88cd1f4f..0000000000 --- a/plugins/antigravity-provider/test/fixtures/help.capture.ndjson +++ /dev/null @@ -1,2 +0,0 @@ -{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--help"], "cwd": "", "home": "", "settings": null, "env": {}}} -{"t_ms": 209, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/help.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/help.stderr.ndjson deleted file mode 100644 index 3a8e68bf4b..0000000000 --- a/plugins/antigravity-provider/test/fixtures/help.stderr.ndjson +++ /dev/null @@ -1,39 +0,0 @@ -{"t_ms": 195, "stream": "stderr", "data": "Usage of agy:"} -{"t_ms": 195, "stream": "stderr", "data": " --add-dir Add a directory to the workspace (repeatable) (default [])"} -{"t_ms": 195, "stream": "stderr", "data": " --agent Agent for the current CLI session"} -{"t_ms": 195, "stream": "stderr", "data": " -c Short alias for --continue"} -{"t_ms": 195, "stream": "stderr", "data": " --continue Continue the most recent conversation"} -{"t_ms": 195, "stream": "stderr", "data": " --conversation Resume a previous conversation by ID"} -{"t_ms": 195, "stream": "stderr", "data": " --dangerously-skip-permissions Auto-approve all tool permission requests without prompting"} -{"t_ms": 195, "stream": "stderr", "data": " --disable-slash-commands Disable slash command and skill expansion in print mode"} -{"t_ms": 195, "stream": "stderr", "data": " --effort Reasoning effort for the current CLI session (low|medium|high|max)"} -{"t_ms": 195, "stream": "stderr", "data": " -i Short alias for --prompt-interactive"} -{"t_ms": 195, "stream": "stderr", "data": " --input-format Input format for print mode (text, stream-json). stream-json reads one NDJSON message per line from stdin and runs a turn for each; it requires --output-format stream-json (default text)"} -{"t_ms": 195, "stream": "stderr", "data": " --json-schema Optional JSON schema string or path to a schema file to enforce structured output (for stream-json, only applicable to the final result)"} -{"t_ms": 195, "stream": "stderr", "data": " --log-file Override CLI log file path"} -{"t_ms": 196, "stream": "stderr", "data": " --mode Set the agent execution mode for this session (accept-edits, plan)"} -{"t_ms": 196, "stream": "stderr", "data": " --model Model for the current CLI session"} -{"t_ms": 196, "stream": "stderr", "data": " --new-project Create a new project for this session"} -{"t_ms": 196, "stream": "stderr", "data": " --output-format Output format for print mode (text, json, stream-json) (default text)"} -{"t_ms": 196, "stream": "stderr", "data": " -p Short alias for --print"} -{"t_ms": 196, "stream": "stderr", "data": " --print Run a single prompt non-interactively and print the response"} -{"t_ms": 196, "stream": "stderr", "data": " --print-timeout Optional time limit for print mode; 0 waits until the turn completes (default 0s)"} -{"t_ms": 196, "stream": "stderr", "data": " --project Project ID or project name for the current CLI session"} -{"t_ms": 196, "stream": "stderr", "data": " --prompt Alias for --print"} -{"t_ms": 196, "stream": "stderr", "data": " --prompt-interactive Run an initial prompt interactively and continue the session"} -{"t_ms": 196, "stream": "stderr", "data": " --remote-control Create a remote connection for the CLI session on start up"} -{"t_ms": 196, "stream": "stderr", "data": " --sandbox Run in a sandbox with terminal restrictions enabled"} -{"t_ms": 196, "stream": "stderr", "data": ""} -{"t_ms": 196, "stream": "stderr", "data": "Available subcommands:"} -{"t_ms": 196, "stream": "stderr", "data": " agent List available agents"} -{"t_ms": 196, "stream": "stderr", "data": " agents List available agents"} -{"t_ms": 196, "stream": "stderr", "data": " changelog Show changelog and release notes"} -{"t_ms": 196, "stream": "stderr", "data": " help Show help for subcommands"} -{"t_ms": 196, "stream": "stderr", "data": " install Configure environment paths and shell settings"} -{"t_ms": 196, "stream": "stderr", "data": " mcp Manage MCP servers (add, remove, list, enable, disable)"} -{"t_ms": 196, "stream": "stderr", "data": " mic-serve Serve this machine's microphone to a CLI on another host"} -{"t_ms": 196, "stream": "stderr", "data": " models List available models"} -{"t_ms": 196, "stream": "stderr", "data": " plugin Manage plugins (install, uninstall, list, enable, disable)"} -{"t_ms": 196, "stream": "stderr", "data": " plugins Alias for plugin"} -{"t_ms": 196, "stream": "stderr", "data": " remote-control Manage the remote-control background daemon (start, status, stop)"} -{"t_ms": 196, "stream": "stderr", "data": " update Update CLI"} diff --git a/plugins/antigravity-provider/test/fixtures/help.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/help.stdin.ndjson deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/plugins/antigravity-provider/test/fixtures/help.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/help.stdout.ndjson deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/plugins/antigravity-provider/test/fixtures/image-content.capture.ndjson b/plugins/antigravity-provider/test/fixtures/image-content.capture.ndjson deleted file mode 100644 index a2351ec906..0000000000 --- a/plugins/antigravity-provider/test/fixtures/image-content.capture.ndjson +++ /dev/null @@ -1,2 +0,0 @@ -{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--disable-slash-commands"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} -{"t_ms": 383, "stream": "capture", "data": {"exit_code": 1}} diff --git a/plugins/antigravity-provider/test/fixtures/image-content.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/image-content.stderr.ndjson deleted file mode 100644 index 3224b28fcc..0000000000 --- a/plugins/antigravity-provider/test/fixtures/image-content.stderr.ndjson +++ /dev/null @@ -1 +0,0 @@ -{"t_ms": 294, "stream": "stderr", "data": "error: stream input content block type \"image\" is not supported (only \"text\")"} diff --git a/plugins/antigravity-provider/test/fixtures/image-content.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/image-content.stdin.ndjson deleted file mode 100644 index fd1966ad02..0000000000 --- a/plugins/antigravity-provider/test/fixtures/image-content.stdin.ndjson +++ /dev/null @@ -1 +0,0 @@ -{"t_ms": 294, "stream": "stdin", "data": {"event": "user", "message": {"content": [{"type": "text", "text": "Describe this image."}, {"type": "image", "source": {"type": "base64", "media_type": "image/png", "data": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aN1sAAAAASUVORK5CYII="}}]}}} diff --git a/plugins/antigravity-provider/test/fixtures/image-content.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/image-content.stdout.ndjson deleted file mode 100644 index 07cb4f0703..0000000000 --- a/plugins/antigravity-provider/test/fixtures/image-content.stdout.ndjson +++ /dev/null @@ -1,2 +0,0 @@ -{"t_ms": 294, "stream": "stdout", "data": {"event": "init", "conversation_id": "71e19e79-63e3-4a7b-b628-822e69618edc", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "request-review"}}} -{"t_ms": 294, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "71e19e79-63e3-4a7b-b628-822e69618edc", "status": "ERROR", "response": "", "error": "stream input content block type \"image\" is not supported (only \"text\")", "duration_seconds": 0, "num_turns": 0, "usage": {"input_tokens": 0, "output_tokens": 0, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 0}}}} diff --git a/plugins/antigravity-provider/test/fixtures/image-file-external.capture.ndjson b/plugins/antigravity-provider/test/fixtures/image-file-external.capture.ndjson deleted file mode 100644 index 90d75443bc..0000000000 --- a/plugins/antigravity-provider/test/fixtures/image-file-external.capture.ndjson +++ /dev/null @@ -1,2 +0,0 @@ -{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", "", "--disable-slash-commands"], "cwd": "", "home": "/image-file-external-home", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} -{"t_ms": 7877, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/image-file-external.png b/plugins/antigravity-provider/test/fixtures/image-file-external.png deleted file mode 100644 index de622fdd72dd12f017ac61ed74038f2e449c7422..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 910 zcmeAS@N?(olHy`uVBq!ia0vp^TR@nD2}o{QKQWbofjQLE#WAE}&fB{Ob3+_OTmso2 zDx9#_xb#CIGO1Bx!~c@t>{Uzns*Pi$^rMdqwSfOd*oe2kzpL`IortR@I9=kr) z_9R&u{!sRzmB1sZJaN$!O@(+?iM_8{PmOrNjOj~n7L zU9yKZhO)`e1}YS`;5+dk*&VF#cA?DVgU3Gsov7b;*yu-uv`km-k-&)uj~@aGiT5qy zu?qv*7F}>9^}|LL3z25$jbANZsr`r$mFdzw7I^&tSe|S`~`?KWu0*!A1eZMZ%-_uiWx@#_Uww$FLFSTA`pzr}-&ssA_CH@;H)_Os#f z!dFt;et{^F2NF?t59Y}i+*#kp(6{g^tIP++<7Z!eK5V%C*PjBOwfp``0_9fP*~k>H z=I1|t=hb15%!h(AYe^G&@#^;bX@v(1cLPlY>a~|yyqf!l*5jtf6JK#Wkcho|&`wfZ zhCggM!|@ldq|8q4cn}eE_XrW`a}6ZO@AWbS$uAX%xe3?$vxD+0-Q z|Fe&jEzG`MG19Z(`_iLh*Yr5x{86BgpZWbG>q$z&Br1NxF8WsJOxBLHOTc`_;OXk; Jvd$@?2>_QFt-JsL diff --git a/plugins/antigravity-provider/test/fixtures/image-file-external.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/image-file-external.stderr.ndjson deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/plugins/antigravity-provider/test/fixtures/image-file-external.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/image-file-external.stdin.ndjson deleted file mode 100644 index 8dcd1a238a..0000000000 --- a/plugins/antigravity-provider/test/fixtures/image-file-external.stdin.ndjson +++ /dev/null @@ -1 +0,0 @@ -{"t_ms": 320, "stream": "stdin", "data": {"event": "user", "message": {"content": "Use view_file to open the attached image at /owned/attachment.png. Describe the shapes and colors visible. Do not use shell or code to infer the pixels."}}} diff --git a/plugins/antigravity-provider/test/fixtures/image-file-external.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/image-file-external.stdout.ndjson deleted file mode 100644 index 77f1580b85..0000000000 --- a/plugins/antigravity-provider/test/fixtures/image-file-external.stdout.ndjson +++ /dev/null @@ -1,14 +0,0 @@ -{"t_ms": 320, "stream": "stdout", "data": {"event": "init", "conversation_id": "0c498203-4202-446f-8df1-dfa8f16cb19d", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "request-review"}}} -{"t_ms": 347, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0c498203-4202-446f-8df1-dfa8f16cb19d", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} -{"t_ms": 4057, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0c498203-4202-446f-8df1-dfa8f16cb19d", "step_index": 1, "state": "DONE", "step_type": "agent_response", "duration_seconds": 3.704260379, "usage": {"input_tokens": 11275, "output_tokens": 384, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11659}}}} -{"t_ms": 4057, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0c498203-4202-446f-8df1-dfa8f16cb19d", "step_index": 2, "state": "ACTIVE", "step_type": "tool", "tool_name": "view_file", "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/owned/attachment.png"}}}}} -{"t_ms": 4057, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0c498203-4202-446f-8df1-dfa8f16cb19d", "step_index": 2, "state": "DONE", "step_type": "tool", "tool_name": "view_file", "duration_seconds": 0.003012462, "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/owned/attachment.png"}}}}} -{"t_ms": 7530, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0c498203-4202-446f-8df1-dfa8f16cb19d", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "Based on the image at [/"}}} -{"t_ms": 7555, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0c498203-4202-446f-8df1-dfa8f16cb19d", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "tmp/agy-p2/o"}}} -{"t_ms": 7581, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0c498203-4202-446f-8df1-dfa8f16cb19d", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "wned/attachment.png](fil"}}} -{"t_ms": 7606, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0c498203-4202-446f-8df1-dfa8f16cb19d", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "e:///tmp/agy"}}} -{"t_ms": 7631, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0c498203-4202-446f-8df1-dfa8f16cb19d", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "-p2/owned/attachment.png"}}} -{"t_ms": 7657, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0c498203-4202-446f-8df1-dfa8f16cb19d", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "), here are "}}} -{"t_ms": 7682, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0c498203-4202-446f-8df1-dfa8f16cb19d", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "the visible shapes and colors:\n\n- **Red Triangl"}}} -{"t_ms": 7708, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0c498203-4202-446f-8df1-dfa8f16cb19d", "step_index": 3, "state": "DONE", "step_type": "agent_response", "text_delta": "e**: A solid red triangle positioned on the left side, pointing upwards.\n- **Blue Circle**: A solid blue circle positioned on the lower right side.\n- **Background**: Solid white.\n", "duration_seconds": 3.629267516, "usage": {"input_tokens": 13749, "output_tokens": 170, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 13919}}}} -{"t_ms": 7708, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "0c498203-4202-446f-8df1-dfa8f16cb19d", "status": "SUCCESS", "response": "Based on the image at [/owned/attachment.png](file:///owned/attachment.png), here are the visible shapes and colors:\n\n- **Red Triangle**: A solid red triangle positioned on the left side, pointing upwards.\n- **Blue Circle**: A solid blue circle positioned on the lower right side.\n- **Background**: Solid white.\n", "duration_seconds": 7.367115828, "num_turns": 1, "usage": {"input_tokens": 25024, "output_tokens": 554, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 25578}}}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-add-http.capture.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-add-http.capture.ndjson deleted file mode 100644 index c04b13268d..0000000000 --- a/plugins/antigravity-provider/test/fixtures/mcp-add-http.capture.ndjson +++ /dev/null @@ -1,3 +0,0 @@ -{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "mcp", "add", "phase0", "http://127.0.0.1:/mcp"], "cwd": "", "home": "", "settings": null, "env": {}}} -{"t_ms": 68, "stream": "capture", "data": {"written_file": "/.gemini/config/mcp_config.json", "content": {"mcpServers": {"phase0": {"disabled": false, "serverUrl": "http://127.0.0.1:/mcp"}}}}} -{"t_ms": 69, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-add-http.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-add-http.stderr.ndjson deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/plugins/antigravity-provider/test/fixtures/mcp-add-http.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-add-http.stdin.ndjson deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/plugins/antigravity-provider/test/fixtures/mcp-add-http.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-add-http.stdout.ndjson deleted file mode 100644 index 05d385caa8..0000000000 --- a/plugins/antigravity-provider/test/fixtures/mcp-add-http.stdout.ndjson +++ /dev/null @@ -1 +0,0 @@ -{"t_ms": 63, "stream": "stdout", "data": "Added MCP server \"phase0\" (http)"} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-add.capture.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-add.capture.ndjson deleted file mode 100644 index d115585bec..0000000000 --- a/plugins/antigravity-provider/test/fixtures/mcp-add.capture.ndjson +++ /dev/null @@ -1,2 +0,0 @@ -{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "mcp", "add", "phase0", "--url", "http://127.0.0.1:/mcp"], "cwd": "", "home": "", "settings": null, "env": {}}} -{"t_ms": 82, "stream": "capture", "data": {"exit_code": 1}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-add.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-add.stderr.ndjson deleted file mode 100644 index 81181df217..0000000000 --- a/plugins/antigravity-provider/test/fixtures/mcp-add.stderr.ndjson +++ /dev/null @@ -1 +0,0 @@ -{"t_ms": 76, "stream": "stderr", "data": "Error: invalid command \"--url\": flags must come before the server name (usage: mcp add [flags] [args...])"} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-add.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-add.stdin.ndjson deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/plugins/antigravity-provider/test/fixtures/mcp-add.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-add.stdout.ndjson deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/plugins/antigravity-provider/test/fixtures/mcp-agent-definition.capture.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-agent-definition.capture.ndjson deleted file mode 100644 index 0885d233a5..0000000000 --- a/plugins/antigravity-provider/test/fixtures/mcp-agent-definition.capture.ndjson +++ /dev/null @@ -1,2 +0,0 @@ -{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", "/owned", "--add-dir", "", "--agent", "phase2", "--dangerously-skip-permissions"], "cwd": "", "home": "/mcp-agent-definition-home", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} -{"t_ms": 6091, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-agent-definition.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-agent-definition.stderr.ndjson deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/plugins/antigravity-provider/test/fixtures/mcp-agent-definition.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-agent-definition.stdin.ndjson deleted file mode 100644 index 8daf16fd52..0000000000 --- a/plugins/antigravity-provider/test/fixtures/mcp-agent-definition.stdin.ndjson +++ /dev/null @@ -1 +0,0 @@ -{"t_ms": 311, "stream": "stdin", "data": {"event": "user", "message": {"content": "Call the phase2 MCP echo tool with text PHASE2_DISCOVERY. Then use run_command pwd and report the workspace directory. Do not choose a different directory."}}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-agent-definition.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-agent-definition.stdout.ndjson deleted file mode 100644 index 854e8701ce..0000000000 --- a/plugins/antigravity-provider/test/fixtures/mcp-agent-definition.stdout.ndjson +++ /dev/null @@ -1,15 +0,0 @@ -{"t_ms": 311, "stream": "stdout", "data": {"event": "init", "conversation_id": "7240f716-f2bd-4641-b210-a4a282a756f7", "init": {"cwd": "", "agent": "phase2", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "always-proceed"}}} -{"t_ms": 340, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7240f716-f2bd-4641-b210-a4a282a756f7", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} -{"t_ms": 4006, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7240f716-f2bd-4641-b210-a4a282a756f7", "step_index": 1, "state": "DONE", "step_type": "agent_response", "duration_seconds": 3.658926744, "usage": {"input_tokens": 11286, "output_tokens": 361, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11647}}}} -{"t_ms": 4006, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7240f716-f2bd-4641-b210-a4a282a756f7", "step_index": 2, "state": "ACTIVE", "step_type": "tool", "tool_name": "run_command", "tool_info": {"name": "run_command", "parameters": {"CommandLine": "pwd"}}}}} -{"t_ms": 4006, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7240f716-f2bd-4641-b210-a4a282a756f7", "step_index": 2, "state": "DONE", "step_type": "tool", "tool_name": "run_command", "duration_seconds": 0.008819631, "tool_info": {"name": "run_command", "parameters": {"CommandLine": "pwd"}, "output": "/owned\r\n"}}}} -{"t_ms": 5473, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7240f716-f2bd-4641-b210-a4a282a756f7", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "The "}}} -{"t_ms": 5603, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7240f716-f2bd-4641-b210-a4a282a756f7", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "`phase2 MCP echo tool` is not availa"}}} -{"t_ms": 5626, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7240f716-f2bd-4641-b210-a4a282a756f7", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ble in the tool definiti"}}} -{"t_ms": 5652, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7240f716-f2bd-4641-b210-a4a282a756f7", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ons provided"}}} -{"t_ms": 5678, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7240f716-f2bd-4641-b210-a4a282a756f7", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " in this"}}} -{"t_ms": 5856, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7240f716-f2bd-4641-b210-a4a282a756f7", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " environment.\n\nRunning `"}}} -{"t_ms": 5881, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7240f716-f2bd-4641-b210-a4a282a756f7", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "pwd` in the workspace re"}}} -{"t_ms": 5907, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7240f716-f2bd-4641-b210-a4a282a756f7", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ports the directory:\n`/t"}}} -{"t_ms": 5932, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7240f716-f2bd-4641-b210-a4a282a756f7", "step_index": 3, "state": "DONE", "step_type": "agent_response", "text_delta": "mp/agy-p2/owned`\n", "duration_seconds": 1.928095705, "usage": {"input_tokens": 11417, "output_tokens": 43, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11460}}}} -{"t_ms": 5932, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "7240f716-f2bd-4641-b210-a4a282a756f7", "status": "SUCCESS", "response": "The `phase2 MCP echo tool` is not available in the tool definitions provided in this environment.\n\nRunning `pwd` in the workspace reports the directory:\n`/owned`\n", "duration_seconds": 5.61440038, "num_turns": 1, "usage": {"input_tokens": 22703, "output_tokens": 404, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 23107}}}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-agent-discovered.capture.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-agent-discovered.capture.ndjson deleted file mode 100644 index c5d6ddd6fd..0000000000 --- a/plugins/antigravity-provider/test/fixtures/mcp-agent-discovered.capture.ndjson +++ /dev/null @@ -1,2 +0,0 @@ -{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", "/owned", "--add-dir", "", "--agent", "phase2", "--dangerously-skip-permissions"], "cwd": "", "home": "/mcp-agent-discovered-home", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} -{"t_ms": 5524, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-agent-discovered.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-agent-discovered.stderr.ndjson deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/plugins/antigravity-provider/test/fixtures/mcp-agent-discovered.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-agent-discovered.stdin.ndjson deleted file mode 100644 index d8cfdf92de..0000000000 --- a/plugins/antigravity-provider/test/fixtures/mcp-agent-discovered.stdin.ndjson +++ /dev/null @@ -1 +0,0 @@ -{"t_ms": 324, "stream": "stdin", "data": {"event": "user", "message": {"content": "Call the phase2 MCP echo tool with text PHASE2_DISCOVERY. Then use run_command pwd and report the workspace directory. Do not choose a different directory."}}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-agent-discovered.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-agent-discovered.stdout.ndjson deleted file mode 100644 index a275715465..0000000000 --- a/plugins/antigravity-provider/test/fixtures/mcp-agent-discovered.stdout.ndjson +++ /dev/null @@ -1,19 +0,0 @@ -{"t_ms": 324, "stream": "stdout", "data": {"event": "init", "conversation_id": "b4d0a25b-bb0a-4495-84af-247f689ba6ad", "init": {"cwd": "", "agent": "phase2", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "always-proceed"}}} -{"t_ms": 356, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b4d0a25b-bb0a-4495-84af-247f689ba6ad", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} -{"t_ms": 3842, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b4d0a25b-bb0a-4495-84af-247f689ba6ad", "step_index": 1, "state": "DONE", "step_type": "agent_response", "duration_seconds": 3.487178538, "usage": {"input_tokens": 11279, "output_tokens": 433, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11712}}}} -{"t_ms": 3842, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b4d0a25b-bb0a-4495-84af-247f689ba6ad", "step_index": 2, "state": "ACTIVE", "step_type": "tool", "tool_name": "run_command", "tool_info": {"name": "run_command", "parameters": {"CommandLine": "pwd"}}}}} -{"t_ms": 3867, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b4d0a25b-bb0a-4495-84af-247f689ba6ad", "step_index": 2, "state": "DONE", "step_type": "tool", "tool_name": "run_command", "duration_seconds": 0.008305961, "tool_info": {"name": "run_command", "parameters": {"CommandLine": "pwd"}, "output": "/owned\r\n"}}}} -{"t_ms": 5050, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b4d0a25b-bb0a-4495-84af-247f689ba6ad", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "1."}}} -{"t_ms": 5177, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b4d0a25b-bb0a-4495-84af-247f689ba6ad", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " **MCP Echo "}}} -{"t_ms": 5203, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b4d0a25b-bb0a-4495-84af-247f689ba6ad", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "Tool**: There is no MCP "}}} -{"t_ms": 5228, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b4d0a25b-bb0a-4495-84af-247f689ba6ad", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "`echo` tool "}}} -{"t_ms": 5254, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b4d0a25b-bb0a-4495-84af-247f689ba6ad", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "registered or available "}}} -{"t_ms": 5279, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b4d0a25b-bb0a-4495-84af-247f689ba6ad", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "in the current environme"}}} -{"t_ms": 5304, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b4d0a25b-bb0a-4495-84af-247f689ba6ad", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "nt/tool definit"}}} -{"t_ms": 5330, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b4d0a25b-bb0a-4495-84af-247f689ba6ad", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ions.\n2. **Workspace Dir"}}} -{"t_ms": 5355, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b4d0a25b-bb0a-4495-84af-247f689ba6ad", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ectory**: Ra"}}} -{"t_ms": 5381, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b4d0a25b-bb0a-4495-84af-247f689ba6ad", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "n `pwd` in the current w"}}} -{"t_ms": 5406, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b4d0a25b-bb0a-4495-84af-247f689ba6ad", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "orkspace directory and o"}}} -{"t_ms": 5432, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b4d0a25b-bb0a-4495-84af-247f689ba6ad", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "btained:\n ```\n /tmp/"}}} -{"t_ms": 5457, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b4d0a25b-bb0a-4495-84af-247f689ba6ad", "step_index": 3, "state": "DONE", "step_type": "agent_response", "text_delta": "agy-p2/owned\n ```\n", "duration_seconds": 1.60141087, "usage": {"input_tokens": 11407, "output_tokens": 62, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11469}}}} -{"t_ms": 5457, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "b4d0a25b-bb0a-4495-84af-247f689ba6ad", "status": "SUCCESS", "response": "1. **MCP Echo Tool**: There is no MCP `echo` tool registered or available in the current environment/tool definitions.\n2. **Workspace Directory**: Ran `pwd` in the current workspace directory and obtained:\n ```\n /owned\n ```\n", "duration_seconds": 5.118791831, "num_turns": 1, "usage": {"input_tokens": 22686, "output_tokens": 495, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 23181}}}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-agent-file.capture.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-agent-file.capture.ndjson deleted file mode 100644 index 57d8da6b18..0000000000 --- a/plugins/antigravity-provider/test/fixtures/mcp-agent-file.capture.ndjson +++ /dev/null @@ -1,2 +0,0 @@ -{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--disable-slash-commands", "--dangerously-skip-permissions", "--add-dir", "", "--agent", "/owned/agent.md"], "cwd": "", "home": "/mcp-agent-file-home", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} -{"t_ms": 12172, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-agent-file.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-agent-file.stderr.ndjson deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/plugins/antigravity-provider/test/fixtures/mcp-agent-file.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-agent-file.stdin.ndjson deleted file mode 100644 index 21feb61a76..0000000000 --- a/plugins/antigravity-provider/test/fixtures/mcp-agent-file.stdin.ndjson +++ /dev/null @@ -1 +0,0 @@ -{"t_ms": 305, "stream": "stdin", "data": {"event": "user", "message": {"content": "First call the phase2 MCP echo tool with text PHASE2_DISCOVERY. If unavailable say MCP_UNAVAILABLE. Then use shell pwd, read input.txt and write output.txt containing PHASE2_WRITE. Use the workspace default directory, without me supplying absolute paths. Report the exact paths used."}}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-agent-file.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-agent-file.stdout.ndjson deleted file mode 100644 index 3d172dbe90..0000000000 --- a/plugins/antigravity-provider/test/fixtures/mcp-agent-file.stdout.ndjson +++ /dev/null @@ -1,39 +0,0 @@ -{"t_ms": 305, "stream": "stdout", "data": {"event": "init", "conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "init": {"cwd": "", "agent": "/owned/agent.md", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "always-proceed"}}} -{"t_ms": 334, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} -{"t_ms": 4780, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 1, "state": "DONE", "step_type": "agent_response", "duration_seconds": 4.446916168, "usage": {"input_tokens": 11298, "output_tokens": 523, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11821}}}} -{"t_ms": 4780, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 2, "state": "ACTIVE", "step_type": "tool", "tool_name": "run_command", "tool_info": {"name": "run_command", "parameters": {"CommandLine": "pwd"}}}}} -{"t_ms": 4780, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 2, "state": "DONE", "step_type": "tool", "tool_name": "run_command", "duration_seconds": 0.007696421, "tool_info": {"name": "run_command", "parameters": {"CommandLine": "pwd"}, "output": "\r\n"}}}} -{"t_ms": 6843, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 3, "state": "DONE", "step_type": "agent_response", "duration_seconds": 2.062108191, "usage": {"input_tokens": 11436, "output_tokens": 47, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11483}}}} -{"t_ms": 6843, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 4, "state": "ACTIVE", "step_type": "tool", "tool_name": "view_file", "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/input.txt"}}}}} -{"t_ms": 6869, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 4, "state": "DONE", "step_type": "tool", "tool_name": "view_file", "duration_seconds": 0.002313647, "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/input.txt"}, "output": "1 lines, 23 bytes"}}}} -{"t_ms": 8781, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 5, "state": "DONE", "step_type": "agent_response", "duration_seconds": 1.919162211, "usage": {"input_tokens": 11654, "output_tokens": 79, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11733}}}} -{"t_ms": 8781, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 6, "state": "ACTIVE", "step_type": "tool", "tool_name": "write_to_file", "tool_info": {"name": "write_to_file", "parameters": {"TargetFile": "/output.txt"}}}}} -{"t_ms": 8781, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 6, "state": "DONE", "step_type": "tool", "tool_name": "write_to_file", "duration_seconds": 0.003384418, "tool_info": {"name": "write_to_file", "parameters": {"TargetFile": "/output.txt"}}}}} -{"t_ms": 11046, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "MCP_"}}} -{"t_ms": 11224, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "UNAVAILABLE\n\n### Execution Summary\n\n"}}} -{"t_ms": 11249, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "1. **MCP Ech"}}} -{"t_ms": 11275, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "o Tool Check**:\n"}}} -{"t_ms": 11402, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " - Status: MCP echo to"}}} -{"t_ms": 11427, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ol was not a"}}} -{"t_ms": 11452, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "vailable in the active e"}}} -{"t_ms": 11478, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "nvironment tools (`MCP_U"}}} -{"t_ms": 11504, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "NAVAILABLE`).\n\n2. **Work"}}} -{"t_ms": 11529, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ing Directory (`pwd`)**:"}}} -{"t_ms": 11554, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "\n - Output"}}} -{"t_ms": 11580, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": ": `"}}} -{"t_ms": 11605, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "`\n\n3. **Read File**:"}}} -{"t_ms": 11630, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "\n - Path: "}}} -{"t_ms": 11656, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "[input.txt](file:///tmp/"}}} -{"t_ms": 11681, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "agy-p2/works"}}} -{"t_ms": 11706, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "pace/"}}} -{"t_ms": 11732, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "input.txt)\n "}}} -{"t_ms": 11757, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " - Content: `PHASE2_WOR"}}} -{"t_ms": 11783, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "KSPACE_MARKER`\n\n4. **Wri"}}} -{"t_ms": 11808, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "tten"}}} -{"t_ms": 11859, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " File**:\n "}}} -{"t_ms": 11885, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "- Path: [output.txt](fil"}}} -{"t_ms": 11910, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "e:///workspac"}}} -{"t_ms": 11936, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "e/output.txt)\n - C"}}} -{"t_ms": 11961, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ontent: `PHASE2_WRITE`"}}} -{"t_ms": 12037, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 3.264948634, "usage": {"input_tokens": 11821, "output_tokens": 155, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11976}}}} -{"t_ms": 12063, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "status": "SUCCESS", "response": "MCP_UNAVAILABLE\n\n### Execution Summary\n\n1. **MCP Echo Tool Check**:\n - Status: MCP echo tool was not available in the active environment tools (`MCP_UNAVAILABLE`).\n\n2. **Working Directory (`pwd`)**:\n - Output: ``\n\n3. **Read File**:\n - Path: [input.txt](file:///input.txt)\n - Content: `PHASE2_WORKSPACE_MARKER`\n\n4. **Written File**:\n - Path: [output.txt](file:///output.txt)\n - Content: `PHASE2_WRITE`\n", "duration_seconds": 11.727995605, "num_turns": 1, "usage": {"input_tokens": 46209, "output_tokens": 804, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 47013}}}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-global.capture.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-global.capture.ndjson deleted file mode 100644 index 452d5e7036..0000000000 --- a/plugins/antigravity-provider/test/fixtures/mcp-global.capture.ndjson +++ /dev/null @@ -1,2 +0,0 @@ -{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", "", "--disable-slash-commands", "--dangerously-skip-permissions"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} -{"t_ms": 9237, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-global.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-global.stderr.ndjson deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/plugins/antigravity-provider/test/fixtures/mcp-global.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-global.stdin.ndjson deleted file mode 100644 index cf779b2f3b..0000000000 --- a/plugins/antigravity-provider/test/fixtures/mcp-global.stdin.ndjson +++ /dev/null @@ -1 +0,0 @@ -{"t_ms": 300, "stream": "stdin", "data": {"event": "user", "message": {"content": "Call the phase0 MCP echo tool with text PHASE0_MCP. Do not use shell. If unavailable say UNAVAILABLE."}}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-global.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-global.stdout.ndjson deleted file mode 100644 index 06b16f0005..0000000000 --- a/plugins/antigravity-provider/test/fixtures/mcp-global.stdout.ndjson +++ /dev/null @@ -1,14 +0,0 @@ -{"t_ms": 300, "stream": "stdout", "data": {"event": "init", "conversation_id": "e21f27e1-de55-4f15-a93f-9f70d726915e", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "always-proceed"}}} -{"t_ms": 328, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e21f27e1-de55-4f15-a93f-9f70d726915e", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} -{"t_ms": 3784, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e21f27e1-de55-4f15-a93f-9f70d726915e", "step_index": 1, "state": "DONE", "step_type": "agent_response", "duration_seconds": 3.433302844, "usage": {"input_tokens": 11924, "output_tokens": 406, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12330}}}} -{"t_ms": 3784, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e21f27e1-de55-4f15-a93f-9f70d726915e", "step_index": 2, "state": "ACTIVE", "step_type": "tool", "tool_name": "view_file", "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/.gemini/antigravity-cli/mcp/phase0/echo.json"}}}}} -{"t_ms": 3784, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e21f27e1-de55-4f15-a93f-9f70d726915e", "step_index": 2, "state": "DONE", "step_type": "tool", "tool_name": "view_file", "duration_seconds": 0.003012222, "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/.gemini/antigravity-cli/mcp/phase0/echo.json"}, "output": "1 lines, 184 bytes"}}}} -{"t_ms": 5749, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e21f27e1-de55-4f15-a93f-9f70d726915e", "step_index": 3, "state": "DONE", "step_type": "agent_response", "duration_seconds": 1.966909166, "usage": {"input_tokens": 12221, "output_tokens": 63, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12284}}}} -{"t_ms": 5749, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e21f27e1-de55-4f15-a93f-9f70d726915e", "step_index": 4, "state": "ACTIVE", "step_type": "tool", "tool_name": "call_mcp_tool", "tool_info": {"name": "call_mcp_tool", "parameters": {"Arguments": "{\"text\": \"PHASE0_MCP\"}", "ServerName": "phase0", "ToolName": "echo"}}}}} -{"t_ms": 5749, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e21f27e1-de55-4f15-a93f-9f70d726915e", "step_index": 4, "state": "ERROR", "step_type": "tool", "tool_name": "call_mcp_tool", "duration_seconds": 0.004579062, "tool_info": {"name": "call_mcp_tool", "parameters": {"Arguments": "{\"text\": \"PHASE0_MCP\"}", "ServerName": "phase0", "ToolName": "echo"}, "error": {"type": "TOOL_ERROR", "message": "calling \"tools/call\": [\n {\n \"expected\": \"record\",\n \"code\": \"invalid_type\",\n \"path\": [\n \"params\",\n \"arguments\"\n ],\n \"message\": \"Invalid input: expected record, received string\"\n }\n]"}}}}} -{"t_ms": 7536, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e21f27e1-de55-4f15-a93f-9f70d726915e", "step_index": 5, "state": "DONE", "step_type": "agent_response", "duration_seconds": 1.781658824, "usage": {"input_tokens": 12407, "output_tokens": 60, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12467}}}} -{"t_ms": 7536, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e21f27e1-de55-4f15-a93f-9f70d726915e", "step_index": 6, "state": "ACTIVE", "step_type": "tool", "tool_name": "call_mcp_tool", "tool_info": {"name": "call_mcp_tool", "parameters": {"Arguments": "{\"text\":\"PHASE0_MCP\"}", "ServerName": "phase0", "ToolName": "echo"}}}}} -{"t_ms": 7536, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e21f27e1-de55-4f15-a93f-9f70d726915e", "step_index": 6, "state": "ERROR", "step_type": "tool", "tool_name": "call_mcp_tool", "duration_seconds": 0.003570676, "tool_info": {"name": "call_mcp_tool", "parameters": {"Arguments": "{\"text\":\"PHASE0_MCP\"}", "ServerName": "phase0", "ToolName": "echo"}, "error": {"type": "TOOL_ERROR", "message": "calling \"tools/call\": [\n {\n \"expected\": \"record\",\n \"code\": \"invalid_type\",\n \"path\": [\n \"params\",\n \"arguments\"\n ],\n \"message\": \"Invalid input: expected record, received string\"\n }\n]"}}}}} -{"t_ms": 8744, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e21f27e1-de55-4f15-a93f-9f70d726915e", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "UNAVAILABLE"}}} -{"t_ms": 9073, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e21f27e1-de55-4f15-a93f-9f70d726915e", "step_index": 7, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 1.522542972, "usage": {"input_tokens": 12590, "output_tokens": 2, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12592}}}} -{"t_ms": 9073, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "e21f27e1-de55-4f15-a93f-9f70d726915e", "status": "SUCCESS", "response": "UNAVAILABLE\n", "duration_seconds": 8.74880144, "num_turns": 1, "usage": {"input_tokens": 49142, "output_tokens": 531, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 49673}}}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-help.capture.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-help.capture.ndjson deleted file mode 100644 index 64ea60355d..0000000000 --- a/plugins/antigravity-provider/test/fixtures/mcp-help.capture.ndjson +++ /dev/null @@ -1,2 +0,0 @@ -{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "mcp", "add", "--help"], "cwd": "", "home": "", "settings": null, "env": {}}} -{"t_ms": 75, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-help.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-help.stderr.ndjson deleted file mode 100644 index 93f2f5abb9..0000000000 --- a/plugins/antigravity-provider/test/fixtures/mcp-help.stderr.ndjson +++ /dev/null @@ -1,28 +0,0 @@ -{"t_ms": 69, "stream": "stderr", "data": "Usage: agy mcp add [flags] [args...]"} -{"t_ms": 69, "stream": "stderr", "data": ""} -{"t_ms": 69, "stream": "stderr", "data": "Add or update an MCP server configuration"} -{"t_ms": 69, "stream": "stderr", "data": ""} -{"t_ms": 69, "stream": "stderr", "data": "Flags:"} -{"t_ms": 69, "stream": "stderr", "data": " -H Short alias for --header (default [])"} -{"t_ms": 69, "stream": "stderr", "data": " -e Short alias for --env (default [])"} -{"t_ms": 69, "stream": "stderr", "data": " --env Environment variable KEY=value (repeatable) (default [])"} -{"t_ms": 69, "stream": "stderr", "data": " -h Show help"} -{"t_ms": 69, "stream": "stderr", "data": " --header HTTP header Key: Value (repeatable) (default [])"} -{"t_ms": 69, "stream": "stderr", "data": " --help Show help"} -{"t_ms": 69, "stream": "stderr", "data": " -t Short alias for --type (default stdio)"} -{"t_ms": 69, "stream": "stderr", "data": " --type Server type: 'stdio' or 'http' (default stdio)"} -{"t_ms": 69, "stream": "stderr", "data": ""} -{"t_ms": 69, "stream": "stderr", "data": "Arguments:"} -{"t_ms": 69, "stream": "stderr", "data": " Unique name for the server."} -{"t_ms": 69, "stream": "stderr", "data": " Executable for a stdio server, or the URL for an http server."} -{"t_ms": 69, "stream": "stderr", "data": " [args...] Arguments passed to the stdio command."} -{"t_ms": 69, "stream": "stderr", "data": ""} -{"t_ms": 69, "stream": "stderr", "data": "Notes:"} -{"t_ms": 69, "stream": "stderr", "data": " Flags must come before ; a flag placed after it is rejected."} -{"t_ms": 69, "stream": "stderr", "data": " http/https URLs are detected automatically, so --type is usually unnecessary."} -{"t_ms": 69, "stream": "stderr", "data": " Use -- before the command to pass a command or args that begin with '-'."} -{"t_ms": 69, "stream": "stderr", "data": ""} -{"t_ms": 69, "stream": "stderr", "data": "Examples:"} -{"t_ms": 69, "stream": "stderr", "data": " agy mcp add fs npx -y /work"} -{"t_ms": 69, "stream": "stderr", "data": " agy mcp add --header \": \" api https://example.com/mcp"} -{"t_ms": 69, "stream": "stderr", "data": " agy mcp add --env = gh -- docker run -i "} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-help.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-help.stdin.ndjson deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/plugins/antigravity-provider/test/fixtures/mcp-help.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-help.stdout.ndjson deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/plugins/antigravity-provider/test/fixtures/mcp-http.requests.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-http.requests.ndjson deleted file mode 100644 index ebe28a2ddb..0000000000 --- a/plugins/antigravity-provider/test/fixtures/mcp-http.requests.ndjson +++ /dev/null @@ -1,20 +0,0 @@ -{"stream": "http_request", "data": {"direction": "server_to_mcp", "message": {"jsonrpc": "2.0", "id": 1, "method": "server/discover", "params": {"_meta": {"io.modelcontextprotocol/clientCapabilities": {"elicitation": {"form": {}, "url": {}}, "roots": {"listChanged": true}}, "io.modelcontextprotocol/clientInfo": {"name": "antigravity-client", "version": "v1.0.0"}, "io.modelcontextprotocol/protocolVersion": "2026-07-28"}}}}} -{"stream": "http_request", "data": {"direction": "server_to_mcp", "message": {"jsonrpc": "2.0", "id": 2, "method": "initialize", "params": {"clientInfo": {"name": "antigravity-client", "version": "v1.0.0"}, "protocolVersion": "2025-11-25", "capabilities": {"elicitation": {"form": {}, "url": {}}, "roots": {"listChanged": true}}}}}} -{"stream": "http_request", "data": {"direction": "server_to_mcp", "message": {"jsonrpc": "2.0", "method": "notifications/roots/list_changed", "params": {}}}} -{"stream": "http_request", "data": {"direction": "server_to_mcp", "message": {"jsonrpc": "2.0", "method": "notifications/initialized", "params": {}}}} -{"stream": "http_request", "data": {"direction": "server_to_mcp", "message": {"jsonrpc": "2.0", "id": 3, "method": "tools/list", "params": {}}}} -{"stream": "http_request", "data": {"direction": "server_to_mcp", "message": {"jsonrpc": "2.0", "method": "notifications/roots/list_changed", "params": {}}}} -{"stream": "http_request", "data": {"direction": "server_to_mcp", "message": {"jsonrpc": "2.0", "id": 4, "method": "tools/call", "params": {"_meta": {"antigravity.google/artifacts_dir": "/.gemini/antigravity-cli/brain/e21f27e1-de55-4f15-a93f-9f70d726915e", "antigravity.google/conversation_id": "e21f27e1-de55-4f15-a93f-9f70d726915e", "progressToken": "88c11f68-c1b5-46f7-ae6d-1cb662c4ca0c:4"}, "name": "echo", "arguments": "{\"text\": \"PHASE0_MCP\"}"}}}} -{"stream": "http_request", "data": {"direction": "server_to_mcp", "message": {"jsonrpc": "2.0", "id": 5, "method": "tools/call", "params": {"_meta": {"antigravity.google/artifacts_dir": "/.gemini/antigravity-cli/brain/e21f27e1-de55-4f15-a93f-9f70d726915e", "antigravity.google/conversation_id": "e21f27e1-de55-4f15-a93f-9f70d726915e", "progressToken": "88c11f68-c1b5-46f7-ae6d-1cb662c4ca0c:6"}, "name": "echo", "arguments": "{\"text\":\"PHASE0_MCP\"}"}}}} -{"stream": "http_request", "data": {"direction": "server_to_mcp", "message": {"jsonrpc": "2.0", "id": 1, "method": "server/discover", "params": {"_meta": {"io.modelcontextprotocol/clientCapabilities": {"elicitation": {"form": {}, "url": {}}, "roots": {"listChanged": true}}, "io.modelcontextprotocol/clientInfo": {"name": "antigravity-client", "version": "v1.0.0"}, "io.modelcontextprotocol/protocolVersion": "2026-07-28"}}}}} -{"stream": "http_request", "data": {"direction": "server_to_mcp", "message": {"jsonrpc": "2.0", "id": 2, "method": "initialize", "params": {"clientInfo": {"name": "antigravity-client", "version": "v1.0.0"}, "protocolVersion": "2025-11-25", "capabilities": {"elicitation": {"form": {}, "url": {}}, "roots": {"listChanged": true}}}}}} -{"stream": "http_request", "data": {"direction": "server_to_mcp", "message": {"jsonrpc": "2.0", "method": "notifications/initialized", "params": {}}}} -{"stream": "http_request", "data": {"direction": "server_to_mcp", "message": {"jsonrpc": "2.0", "id": 3, "method": "tools/list", "params": {}}}} -{"stream": "http_request", "data": {"direction": "server_to_mcp", "message": {"jsonrpc": "2.0", "id": 4, "method": "tools/call", "params": {"_meta": {"antigravity.google/artifacts_dir": "/.gemini/antigravity-cli/brain/89273b5a-77cd-4d4b-ac80-4475b2c751d0", "antigravity.google/conversation_id": "89273b5a-77cd-4d4b-ac80-4475b2c751d0", "progressToken": "c009d4b2-d4f6-44f3-9a29-1b0092eb123f:4"}, "name": "echo", "arguments": "{\"text\":\"PHASE0_MCP\"}"}}}} -{"stream": "http_request", "data": {"direction": "server_to_mcp", "message": {"jsonrpc": "2.0", "id": 5, "method": "tools/call", "params": {"_meta": {"antigravity.google/artifacts_dir": "/.gemini/antigravity-cli/brain/89273b5a-77cd-4d4b-ac80-4475b2c751d0", "antigravity.google/conversation_id": "89273b5a-77cd-4d4b-ac80-4475b2c751d0", "progressToken": "c009d4b2-d4f6-44f3-9a29-1b0092eb123f:6"}, "name": "echo", "arguments": "{\"text\": \"PHASE0_MCP\"}"}}}} -{"stream": "http_request", "data": {"direction": "server_to_mcp", "message": {"jsonrpc": "2.0", "id": 1, "method": "server/discover", "params": {"_meta": {"io.modelcontextprotocol/clientCapabilities": {"elicitation": {"form": {}, "url": {}}, "roots": {"listChanged": true}}, "io.modelcontextprotocol/clientInfo": {"name": "antigravity-client", "version": "v1.0.0"}, "io.modelcontextprotocol/protocolVersion": "2026-07-28"}}}}} -{"stream": "http_request", "data": {"direction": "server_to_mcp", "message": {"jsonrpc": "2.0", "id": 2, "method": "initialize", "params": {"clientInfo": {"name": "antigravity-client", "version": "v1.0.0"}, "protocolVersion": "2025-11-25", "capabilities": {"elicitation": {"form": {}, "url": {}}, "roots": {"listChanged": true}}}}}} -{"stream": "http_request", "data": {"direction": "server_to_mcp", "message": {"jsonrpc": "2.0", "method": "notifications/initialized", "params": {}}}} -{"stream": "http_request", "data": {"direction": "server_to_mcp", "message": {"jsonrpc": "2.0", "id": 3, "method": "tools/list", "params": {}}}} -{"stream": "http_request", "data": {"direction": "server_to_mcp", "message": {"jsonrpc": "2.0", "id": 4, "method": "tools/call", "params": {"_meta": {"antigravity.google/artifacts_dir": "/.gemini/antigravity-cli/brain/0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "antigravity.google/conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "progressToken": "d3dee97c-770c-4268-86af-baa39125d7c0:4"}, "name": "echo", "arguments": "{\"text\":\"PHASE0_MCP\"}"}}}} -{"stream": "http_request", "data": {"direction": "server_to_mcp", "message": {"jsonrpc": "2.0", "id": 5, "method": "tools/call", "params": {"_meta": {"antigravity.google/artifacts_dir": "/.gemini/antigravity-cli/brain/0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "antigravity.google/conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "progressToken": "d3dee97c-770c-4268-86af-baa39125d7c0:6"}, "name": "echo", "arguments": "null"}}}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-list.capture.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-list.capture.ndjson deleted file mode 100644 index a7a6d66835..0000000000 --- a/plugins/antigravity-provider/test/fixtures/mcp-list.capture.ndjson +++ /dev/null @@ -1,2 +0,0 @@ -{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "mcp", "list"], "cwd": "", "home": "", "settings": null, "env": {}}} -{"t_ms": 71, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-list.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-list.stderr.ndjson deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/plugins/antigravity-provider/test/fixtures/mcp-list.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-list.stdin.ndjson deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/plugins/antigravity-provider/test/fixtures/mcp-list.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-list.stdout.ndjson deleted file mode 100644 index 644935d9f8..0000000000 --- a/plugins/antigravity-provider/test/fixtures/mcp-list.stdout.ndjson +++ /dev/null @@ -1 +0,0 @@ -{"t_ms": 65, "stream": "stdout", "data": "No MCP servers configured."} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-owned-cwd.capture.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-owned-cwd.capture.ndjson deleted file mode 100644 index ec822f7c19..0000000000 --- a/plugins/antigravity-provider/test/fixtures/mcp-owned-cwd.capture.ndjson +++ /dev/null @@ -1,2 +0,0 @@ -{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--disable-slash-commands", "--dangerously-skip-permissions", "--add-dir", ""], "cwd": "/owned-parent/workspace", "home": "/mcp-owned-cwd-home", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}, "cwd_realpath": "/owned"}} -{"t_ms": 21316, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-owned-cwd.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-owned-cwd.stderr.ndjson deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/plugins/antigravity-provider/test/fixtures/mcp-owned-cwd.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-owned-cwd.stdin.ndjson deleted file mode 100644 index 4606a29d9d..0000000000 --- a/plugins/antigravity-provider/test/fixtures/mcp-owned-cwd.stdin.ndjson +++ /dev/null @@ -1 +0,0 @@ -{"t_ms": 299, "stream": "stdin", "data": {"event": "user", "message": {"content": "First call the phase2 MCP echo tool with text PHASE2_DISCOVERY. If unavailable say MCP_UNAVAILABLE. Then use shell pwd, read input.txt and write output.txt containing PHASE2_WRITE. Use the workspace default directory, without me supplying absolute paths. Report the exact paths used."}}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-owned-cwd.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-owned-cwd.stdout.ndjson deleted file mode 100644 index fd49e1756a..0000000000 --- a/plugins/antigravity-provider/test/fixtures/mcp-owned-cwd.stdout.ndjson +++ /dev/null @@ -1,95 +0,0 @@ -{"t_ms": 299, "stream": "stdout", "data": {"event": "init", "conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "init": {"cwd": "/owned", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "always-proceed"}}} -{"t_ms": 353, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} -{"t_ms": 4289, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 1, "state": "DONE", "step_type": "agent_response", "duration_seconds": 3.923817807, "usage": {"input_tokens": 11986, "output_tokens": 312, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12298}}}} -{"t_ms": 4289, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 2, "state": "ACTIVE", "step_type": "tool", "tool_name": "view_file", "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/mcp-owned-cwd-home/.gemini/antigravity-cli/mcp/phase2/echo.json"}}}}} -{"t_ms": 4289, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 2, "state": "DONE", "step_type": "tool", "tool_name": "view_file", "duration_seconds": 0.003110546, "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/mcp-owned-cwd-home/.gemini/antigravity-cli/mcp/phase2/echo.json"}, "output": "1 lines, 184 bytes"}}}} -{"t_ms": 6028, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 3, "state": "DONE", "step_type": "agent_response", "duration_seconds": 1.727561556, "usage": {"input_tokens": 12292, "output_tokens": 64, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12356}}}} -{"t_ms": 6028, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 4, "state": "ACTIVE", "step_type": "tool", "tool_name": "call_mcp_tool", "tool_info": {"name": "call_mcp_tool", "parameters": {"Arguments": "{\"text\": \"PHASE2_DISCOVERY\"}", "ServerName": "phase2", "ToolName": "echo"}}}}} -{"t_ms": 6028, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 4, "state": "ERROR", "step_type": "tool", "tool_name": "call_mcp_tool", "duration_seconds": 0.003994388, "tool_info": {"name": "call_mcp_tool", "parameters": {"Arguments": "{\"text\": \"PHASE2_DISCOVERY\"}", "ServerName": "phase2", "ToolName": "echo"}, "error": {"type": "TOOL_ERROR", "message": "calling \"tools/call\": [\n {\n \"expected\": \"record\",\n \"code\": \"invalid_type\",\n \"path\": [\n \"params\",\n \"arguments\"\n ],\n \"message\": \"Invalid input: expected record, received string\"\n }\n]"}}}}} -{"t_ms": 7764, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 5, "state": "DONE", "step_type": "agent_response", "duration_seconds": 1.73152143, "usage": {"input_tokens": 12479, "output_tokens": 63, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12542}}}} -{"t_ms": 7764, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 6, "state": "ACTIVE", "step_type": "tool", "tool_name": "call_mcp_tool", "tool_info": {"name": "call_mcp_tool", "parameters": {"Arguments": "{\"text\":\"PHASE2_DISCOVERY\"}", "ServerName": "phase2", "ToolName": "echo"}}}}} -{"t_ms": 7764, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 6, "state": "ERROR", "step_type": "tool", "tool_name": "call_mcp_tool", "duration_seconds": 0.003571568, "tool_info": {"name": "call_mcp_tool", "parameters": {"Arguments": "{\"text\":\"PHASE2_DISCOVERY\"}", "ServerName": "phase2", "ToolName": "echo"}, "error": {"type": "TOOL_ERROR", "message": "calling \"tools/call\": [\n {\n \"expected\": \"record\",\n \"code\": \"invalid_type\",\n \"path\": [\n \"params\",\n \"arguments\"\n ],\n \"message\": \"Invalid input: expected record, received string\"\n }\n]"}}}}} -{"t_ms": 9626, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 7, "state": "DONE", "step_type": "agent_response", "duration_seconds": 1.8600456250000001, "usage": {"input_tokens": 12665, "output_tokens": 70, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12735}}}} -{"t_ms": 9626, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 8, "state": "ACTIVE", "step_type": "tool", "tool_name": "run_command", "tool_info": {"name": "run_command", "parameters": {"CommandLine": "pwd"}}}}} -{"t_ms": 9626, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 8, "state": "DONE", "step_type": "tool", "tool_name": "run_command", "duration_seconds": 0.005713043, "tool_info": {"name": "run_command", "parameters": {"CommandLine": "pwd"}, "output": "/owned\r\n"}}}} -{"t_ms": 11152, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 9, "state": "DONE", "step_type": "agent_response", "duration_seconds": 1.5154668610000002, "usage": {"input_tokens": 12800, "output_tokens": 47, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12847}}}} -{"t_ms": 11152, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 10, "state": "ACTIVE", "step_type": "tool", "tool_name": "view_file", "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/owned/input.txt"}}}}} -{"t_ms": 11152, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 10, "state": "ERROR", "step_type": "tool", "tool_name": "view_file", "duration_seconds": 0.002729213, "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/owned/input.txt"}, "error": {"type": "TOOL_ERROR", "message": "declaring permissions: cortex tool view_file: convert tool call for permissions: model output error: invalid tool call error (invalid_args) failed to read file: stat /owned/input.txt: no such file or directory"}}}}} -{"t_ms": 13856, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 11, "state": "DONE", "step_type": "agent_response", "duration_seconds": 2.6989989100000003, "usage": {"input_tokens": 12961, "output_tokens": 48, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 13009}}}} -{"t_ms": 13856, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 12, "state": "ACTIVE", "step_type": "tool", "tool_name": "view_file", "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/input.txt"}}}}} -{"t_ms": 13856, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 12, "state": "DONE", "step_type": "tool", "tool_name": "view_file", "duration_seconds": 0.002359752, "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/input.txt"}, "output": "1 lines, 23 bytes"}}}} -{"t_ms": 15999, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 13, "state": "DONE", "step_type": "agent_response", "duration_seconds": 2.144108405, "usage": {"input_tokens": 13180, "output_tokens": 78, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 13258}}}} -{"t_ms": 15999, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 14, "state": "ACTIVE", "step_type": "tool", "tool_name": "write_to_file", "tool_info": {"name": "write_to_file", "parameters": {"TargetFile": "/owned/output.txt"}}}}} -{"t_ms": 15999, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 14, "state": "DONE", "step_type": "tool", "tool_name": "write_to_file", "duration_seconds": 0.004225731, "tool_info": {"name": "write_to_file", "parameters": {"TargetFile": "/owned/output.txt"}}}}} -{"t_ms": 17563, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 15, "state": "DONE", "step_type": "agent_response", "duration_seconds": 1.5609718259999998, "usage": {"input_tokens": 13346, "output_tokens": 79, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 13425}}}} -{"t_ms": 17563, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 16, "state": "ACTIVE", "step_type": "tool", "tool_name": "write_to_file", "tool_info": {"name": "write_to_file", "parameters": {"TargetFile": "/output.txt"}}}}} -{"t_ms": 17563, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 16, "state": "DONE", "step_type": "tool", "tool_name": "write_to_file", "duration_seconds": 0.003200805, "tool_info": {"name": "write_to_file", "parameters": {"TargetFile": "/output.txt"}}}}} -{"t_ms": 18830, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "### Phase"}}} -{"t_ms": 19110, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " 2 MCP Tool Call\nThe `ph"}}} -{"t_ms": 19134, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ase2` MCP server was cal"}}} -{"t_ms": 19160, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "led with `ec"}}} -{"t_ms": 19185, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ho`, but tool execution "}}} -{"t_ms": 19211, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "returned a p"}}} -{"t_ms": 19237, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "arameter parsing error (`invalid_typ"}}} -{"t_ms": 19262, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "e: expected record, rece"}}} -{"t_ms": 19288, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ived string`"}}} -{"t_ms": 19314, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " when passing arguments)"}}} -{"t_ms": 19339, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": ". Therefore:"}}} -{"t_ms": 19365, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " **MCP_UNAVAILABLE** (or"}}} -{"t_ms": 19390, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " encounterin"}}} -{"t_ms": 19416, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "g invocation schema incompatibilitie"}}} -{"t_ms": 19441, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "s).\n\n---\n\n### Shell `pwd"}}} -{"t_ms": 19466, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "`\nExecuted `"}}} -{"t_ms": 19492, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "pwd` in the default comm"}}} -{"t_ms": 19517, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "and working "}}} -{"t_ms": 19544, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "directory:\n- **Default CWD:** `/tmp/"}}} -{"t_ms": 19570, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "agy-p2/owned`\n\n---\n\n### "}}} -{"t_ms": 19595, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "File Operati"}}} -{"t_ms": 19621, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ons\n1. **Read `input.txt"}}} -{"t_ms": 19646, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "`:**\n - Attempted: [`/o"}}} -{"t_ms": 19672, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "wned/input.t"}}} -{"t_ms": 19698, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "xt`](file:///"}}} -{"t_ms": 19723, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "owned/"}}} -{"t_ms": 19774, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "input.txt) (not present)\n - Read f"}}} -{"t_ms": 19799, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "rom:"}}} -{"t_ms": 19879, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " [`"}}} -{"t_ms": 19905, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "/input.txt`](file:///tmp"}}} -{"t_ms": 19930, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "/agy-p2/work"}}} -{"t_ms": 19956, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "space/input.txt)\n - Content: `PHAS"}}} -{"t_ms": 19981, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "E2_WORKSPACE"}}} -{"t_ms": 20006, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "_MARKER`\n\n2. **Wrote `ou"}}} -{"t_ms": 20032, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "tput.txt`:**\n"}}} -{"t_ms": 20083, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " - Written"}}} -{"t_ms": 20109, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " to: [`/tmp/"}}} -{"t_ms": 20134, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "agy-p2/owned/output.txt`"}}} -{"t_ms": 20160, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "](file:///tmp/agy-p"}}} -{"t_ms": 20185, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "2/owned/output.txt) (def"}}} -{"t_ms": 20211, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ault CWD)\n - Also writ"}}} -{"t_ms": 20237, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ten to:"}}} -{"t_ms": 20364, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " [`/tmp/agy-"}}} -{"t_ms": 20389, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "p2/workspace/output.txt`"}}} -{"t_ms": 20415, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "](file:///wor"}}} -{"t_ms": 20440, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "kspace/outpu"}}} -{"t_ms": 20465, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "t.txt) ("}}} -{"t_ms": 20491, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "active workspace directo"}}} -{"t_ms": 20517, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ry)\n - Content: `PHASE"}}} -{"t_ms": 20542, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "2_WRITE`\n\n---\n\n### Exact"}}} -{"t_ms": 20568, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " Paths Used\n"}}} -{"t_ms": 20644, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "- **Shell CWD:** `/tmp/a"}}} -{"t_ms": 20671, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "gy-p2/owned`\n- **Input p"}}} -{"t_ms": 20695, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ath:**"}}} -{"t_ms": 20747, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " [`"}}} -{"t_ms": 20773, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "/input.txt`](file:///tmp"}}} -{"t_ms": 20799, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "/agy-p2/workspace/input."}}} -{"t_ms": 20824, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "txt"}}} -{"t_ms": 20850, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": ")\n- **Output path:** [`/"}}} -{"t_ms": 20876, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "tmp/agy-p2/o"}}} -{"t_ms": 20901, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "wned/output.txt`](file:/"}}} -{"t_ms": 20927, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "//"}}} -{"t_ms": 20952, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "tmp/agy-p2/o"}}} -{"t_ms": 20978, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "wned/output.txt) &"}}} -{"t_ms": 21105, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " [`"}}} -{"t_ms": 21131, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "/output.txt`"}}} -{"t_ms": 21156, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "](file:///wor"}}} -{"t_ms": 21181, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "DONE", "step_type": "agent_response", "text_delta": "kspace/output.txt)\n", "duration_seconds": 3.625222904, "usage": {"input_tokens": 13513, "output_tokens": 439, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 13952}}}} -{"t_ms": 21207, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "status": "SUCCESS", "response": "### Phase 2 MCP Tool Call\nThe `phase2` MCP server was called with `echo`, but tool execution returned a parameter parsing error (`invalid_type: expected record, received string` when passing arguments). Therefore: **MCP_UNAVAILABLE** (or encountering invocation schema incompatibilities).\n\n---\n\n### Shell `pwd`\nExecuted `pwd` in the default command working directory:\n- **Default CWD:** `/owned`\n\n---\n\n### File Operations\n1. **Read `input.txt`:**\n - Attempted: [`/owned/input.txt`](file:///owned/input.txt) (not present)\n - Read from: [`/input.txt`](file:///input.txt)\n - Content: `PHASE2_WORKSPACE_MARKER`\n\n2. **Wrote `output.txt`:**\n - Written to: [`/owned/output.txt`](file:///owned/output.txt) (default CWD)\n - Also written to: [`/output.txt`](file:///output.txt) (active workspace directory)\n - Content: `PHASE2_WRITE`\n\n---\n\n### Exact Paths Used\n- **Shell CWD:** `/owned`\n- **Input path:** [`/input.txt`](file:///input.txt)\n- **Output path:** [`/owned/output.txt`](file:///owned/output.txt) & [`/output.txt`](file:///output.txt)\n", "duration_seconds": 20.876486639, "num_turns": 1, "usage": {"input_tokens": 115222, "output_tokens": 1200, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 116422}}}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-owned-first-add-dir.capture.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-owned-first-add-dir.capture.ndjson deleted file mode 100644 index ea2081bbd9..0000000000 --- a/plugins/antigravity-provider/test/fixtures/mcp-owned-first-add-dir.capture.ndjson +++ /dev/null @@ -1,2 +0,0 @@ -{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--disable-slash-commands", "--dangerously-skip-permissions", "--add-dir", "/owned", "--add-dir", ""], "cwd": "", "home": "/mcp-owned-first-add-dir-home", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} -{"t_ms": 23197, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-owned-first-add-dir.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-owned-first-add-dir.stderr.ndjson deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/plugins/antigravity-provider/test/fixtures/mcp-owned-first-add-dir.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-owned-first-add-dir.stdin.ndjson deleted file mode 100644 index 1cb2727caa..0000000000 --- a/plugins/antigravity-provider/test/fixtures/mcp-owned-first-add-dir.stdin.ndjson +++ /dev/null @@ -1 +0,0 @@ -{"t_ms": 307, "stream": "stdin", "data": {"event": "user", "message": {"content": "First call the phase2 MCP echo tool with text PHASE2_DISCOVERY. If unavailable say MCP_UNAVAILABLE. Then use shell pwd, read input.txt and write output.txt containing PHASE2_WRITE. Use the workspace default directory, without me supplying absolute paths. Report the exact paths used."}}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-owned-first-add-dir.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-owned-first-add-dir.stdout.ndjson deleted file mode 100644 index 8f9424660b..0000000000 --- a/plugins/antigravity-provider/test/fixtures/mcp-owned-first-add-dir.stdout.ndjson +++ /dev/null @@ -1,60 +0,0 @@ -{"t_ms": 307, "stream": "stdout", "data": {"event": "init", "conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "always-proceed"}}} -{"t_ms": 373, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} -{"t_ms": 3416, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 1, "state": "DONE", "step_type": "agent_response", "duration_seconds": 3.021318824, "usage": {"input_tokens": 12003, "output_tokens": 294, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12297}}}} -{"t_ms": 3416, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 2, "state": "ACTIVE", "step_type": "tool", "tool_name": "view_file", "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/mcp-owned-first-add-dir-home/.gemini/antigravity-cli/mcp/phase2/echo.json"}}}}} -{"t_ms": 3416, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 2, "state": "DONE", "step_type": "tool", "tool_name": "view_file", "duration_seconds": 0.004082433, "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/mcp-owned-first-add-dir-home/.gemini/antigravity-cli/mcp/phase2/echo.json"}, "output": "1 lines, 184 bytes"}}}} -{"t_ms": 5132, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 3, "state": "DONE", "step_type": "agent_response", "duration_seconds": 1.7174826109999999, "usage": {"input_tokens": 12318, "output_tokens": 64, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12382}}}} -{"t_ms": 5133, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 4, "state": "ACTIVE", "step_type": "tool", "tool_name": "call_mcp_tool", "tool_info": {"name": "call_mcp_tool", "parameters": {"Arguments": "{\"text\":\"PHASE2_DISCOVERY\"}", "ServerName": "phase2", "ToolName": "echo"}}}}} -{"t_ms": 5133, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 4, "state": "ERROR", "step_type": "tool", "tool_name": "call_mcp_tool", "duration_seconds": 0.005855559, "tool_info": {"name": "call_mcp_tool", "parameters": {"Arguments": "{\"text\":\"PHASE2_DISCOVERY\"}", "ServerName": "phase2", "ToolName": "echo"}, "error": {"type": "TOOL_ERROR", "message": "calling \"tools/call\": [\n {\n \"expected\": \"record\",\n \"code\": \"invalid_type\",\n \"path\": [\n \"params\",\n \"arguments\"\n ],\n \"message\": \"Invalid input: expected record, received string\"\n }\n]"}}}}} -{"t_ms": 9981, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 5, "state": "DONE", "step_type": "agent_response", "duration_seconds": 4.850317858, "usage": {"input_tokens": 12505, "output_tokens": 68, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12573}}}} -{"t_ms": 9982, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 6, "state": "ACTIVE", "step_type": "tool", "tool_name": "run_command", "tool_info": {"name": "run_command", "parameters": {"CommandLine": "pwd"}}}}} -{"t_ms": 10007, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 6, "state": "DONE", "step_type": "tool", "tool_name": "run_command", "duration_seconds": 0.005511565, "tool_info": {"name": "run_command", "parameters": {"CommandLine": "pwd"}, "output": "/owned\r\n"}}}} -{"t_ms": 11982, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 7, "state": "DONE", "step_type": "agent_response", "duration_seconds": 1.9862325090000001, "usage": {"input_tokens": 12638, "output_tokens": 47, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12685}}}} -{"t_ms": 11982, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 8, "state": "ACTIVE", "step_type": "tool", "tool_name": "view_file", "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/owned/input.txt"}}}}} -{"t_ms": 11982, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 8, "state": "ERROR", "step_type": "tool", "tool_name": "view_file", "duration_seconds": 0.002185687, "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/owned/input.txt"}, "error": {"type": "TOOL_ERROR", "message": "declaring permissions: cortex tool view_file: convert tool call for permissions: model output error: invalid tool call error (invalid_args) failed to read file: stat /owned/input.txt: no such file or directory"}}}}} -{"t_ms": 13701, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 9, "state": "DONE", "step_type": "agent_response", "duration_seconds": 1.704119444, "usage": {"input_tokens": 12799, "output_tokens": 51, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12850}}}} -{"t_ms": 13701, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 10, "state": "ACTIVE", "step_type": "tool", "tool_name": "view_file", "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/input.txt"}}}}} -{"t_ms": 13701, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 10, "state": "DONE", "step_type": "tool", "tool_name": "view_file", "duration_seconds": 0.003250577, "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/input.txt"}, "output": "1 lines, 23 bytes"}}}} -{"t_ms": 15580, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 11, "state": "DONE", "step_type": "agent_response", "duration_seconds": 1.85800527, "usage": {"input_tokens": 13021, "output_tokens": 82, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 13103}}}} -{"t_ms": 15580, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 12, "state": "ACTIVE", "step_type": "tool", "tool_name": "write_to_file", "tool_info": {"name": "write_to_file", "parameters": {"TargetFile": "/owned/output.txt"}}}}} -{"t_ms": 15580, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 12, "state": "DONE", "step_type": "tool", "tool_name": "write_to_file", "duration_seconds": 0.005710137, "tool_info": {"name": "write_to_file", "parameters": {"TargetFile": "/owned/output.txt"}}}}} -{"t_ms": 17976, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 13, "state": "DONE", "step_type": "agent_response", "duration_seconds": 2.381626267, "usage": {"input_tokens": 13191, "output_tokens": 82, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 13273}}}} -{"t_ms": 17976, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 14, "state": "ACTIVE", "step_type": "tool", "tool_name": "write_to_file", "tool_info": {"name": "write_to_file", "parameters": {"TargetFile": "/output.txt"}}}}} -{"t_ms": 17976, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 14, "state": "DONE", "step_type": "tool", "tool_name": "write_to_file", "duration_seconds": 0.002994941, "tool_info": {"name": "write_to_file", "parameters": {"TargetFile": "/output.txt"}}}}} -{"t_ms": 21806, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "1"}}} -{"t_ms": 21908, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": ". **MCP Tool Call (`phas"}}} -{"t_ms": 21939, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "e2` / `echo`):**\n - At"}}} -{"t_ms": 21971, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "tempt"}}} -{"t_ms": 22074, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ed to invoke"}}} -{"t_ms": 22099, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " `phase2` `echo` with te"}}} -{"t_ms": 22127, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "xt `PHASE2_DISCOVERY`. T"}}} -{"t_ms": 22159, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "he MCP tool returned"}}} -{"t_ms": 22213, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " an interface/parameter "}}} -{"t_ms": 22249, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "error (`Invalid input: e"}}} -{"t_ms": 22268, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "xpected record, received"}}} -{"t_ms": 22293, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " string`), i"}}} -{"t_ms": 22319, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ndicating the tool could not process"}}} -{"t_ms": 22349, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " the call: `"}}} -{"t_ms": 22375, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "MCP_UNAVAILABLE`.\n\n2. **"}}} -{"t_ms": 22402, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "Shell `pwd`:**\n - Defa"}}} -{"t_ms": 22430, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ult working "}}} -{"t_ms": 22458, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "directory: `/tmp/"}}} -{"t_ms": 22509, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "agy-p2/owned`\n\n3. **File"}}} -{"t_ms": 22535, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " Operations "}}} -{"t_ms": 22569, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "& Exact Paths:**\n - **"}}} -{"t_ms": 22594, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "Input File Read:**"}}} -{"t_ms": 22619, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " [input.txt](file:///work"}}} -{"t_ms": 22645, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "space/input."}}} -{"t_ms": 22671, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "txt) (content: `PHASE2_"}}} -{"t_ms": 22726, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "WORKSPACE_MARKER`)\n - "}}} -{"t_ms": 22751, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "**Output Files Written:*"}}} -{"t_ms": 22776, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "*\n - [ou"}}} -{"t_ms": 22807, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "tput.txt](file:///owne"}}} -{"t_ms": 22833, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "d/output.txt) (content: "}}} -{"t_ms": 22859, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "`PHASE2_WRITE`)\n -"}}} -{"t_ms": 22911, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " [output.txt](file:///tm"}}} -{"t_ms": 22937, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "p/agy-p2/workspace/outpu"}}} -{"t_ms": 22964, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "t.txt) (content: `PHASE2"}}} -{"t_ms": 22990, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "_WRITE`)"}}} -{"t_ms": 23043, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 5.067703746, "usage": {"input_tokens": 13361, "output_tokens": 225, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 13586}}}} -{"t_ms": 23043, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "status": "SUCCESS", "response": "1. **MCP Tool Call (`phase2` / `echo`):**\n - Attempted to invoke `phase2` `echo` with text `PHASE2_DISCOVERY`. The MCP tool returned an interface/parameter error (`Invalid input: expected record, received string`), indicating the tool could not process the call: `MCP_UNAVAILABLE`.\n\n2. **Shell `pwd`:**\n - Default working directory: `/owned`\n\n3. **File Operations & Exact Paths:**\n - **Input File Read:** [input.txt](file:///input.txt) (content: `PHASE2_WORKSPACE_MARKER`)\n - **Output Files Written:**\n - [output.txt](file:///owned/output.txt) (content: `PHASE2_WRITE`)\n - [output.txt](file:///output.txt) (content: `PHASE2_WRITE`)\n", "duration_seconds": 22.712812313, "num_turns": 1, "usage": {"input_tokens": 101836, "output_tokens": 913, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 102749}}}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-project-config.capture.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-project-config.capture.ndjson deleted file mode 100644 index 97c48693ea..0000000000 --- a/plugins/antigravity-provider/test/fixtures/mcp-project-config.capture.ndjson +++ /dev/null @@ -1,2 +0,0 @@ -{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", "", "--disable-slash-commands", "--dangerously-skip-permissions"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} -{"t_ms": 3445, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-project-config.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-project-config.stderr.ndjson deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/plugins/antigravity-provider/test/fixtures/mcp-project-config.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-project-config.stdin.ndjson deleted file mode 100644 index f5fa2b6685..0000000000 --- a/plugins/antigravity-provider/test/fixtures/mcp-project-config.stdin.ndjson +++ /dev/null @@ -1 +0,0 @@ -{"t_ms": 297, "stream": "stdin", "data": {"event": "user", "message": {"content": "Call the phase0 MCP echo tool with text PHASE0_MCP. Do not use shell. If unavailable say UNAVAILABLE."}}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-project-config.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-project-config.stdout.ndjson deleted file mode 100644 index c77058cdf8..0000000000 --- a/plugins/antigravity-provider/test/fixtures/mcp-project-config.stdout.ndjson +++ /dev/null @@ -1,5 +0,0 @@ -{"t_ms": 297, "stream": "stdout", "data": {"event": "init", "conversation_id": "0e78dd7a-7278-49b7-8cd6-68b9c0dfe82b", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "always-proceed"}}} -{"t_ms": 325, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0e78dd7a-7278-49b7-8cd6-68b9c0dfe82b", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} -{"t_ms": 2917, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0e78dd7a-7278-49b7-8cd6-68b9c0dfe82b", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "UNAVAILABLE"}}} -{"t_ms": 3272, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0e78dd7a-7278-49b7-8cd6-68b9c0dfe82b", "step_index": 1, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 2.949354512, "usage": {"input_tokens": 11261, "output_tokens": 222, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11483}}}} -{"t_ms": 3298, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "0e78dd7a-7278-49b7-8cd6-68b9c0dfe82b", "status": "SUCCESS", "response": "UNAVAILABLE\n", "duration_seconds": 2.972410821, "num_turns": 1, "usage": {"input_tokens": 11261, "output_tokens": 222, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11483}}}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-project-isolated.capture.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-project-isolated.capture.ndjson deleted file mode 100644 index c3f823d332..0000000000 --- a/plugins/antigravity-provider/test/fixtures/mcp-project-isolated.capture.ndjson +++ /dev/null @@ -1,4 +0,0 @@ -{"t_ms": 0, "stream": "capture", "data": {"setup_file": "/.agents/mcp_config.json", "content": {"mcpServers": {"phase0": {"serverUrl": "http://127.0.0.1:/mcp"}}}, "global_mcp_config_exists": false}} -{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", "", "--dangerously-skip-permissions"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} -{"t_ms": 9494, "stream": "capture", "data": {"global_mcp_config_exists": true}} -{"t_ms": 9645, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-project-isolated.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-project-isolated.stderr.ndjson deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/plugins/antigravity-provider/test/fixtures/mcp-project-isolated.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-project-isolated.stdin.ndjson deleted file mode 100644 index 7907cb1235..0000000000 --- a/plugins/antigravity-provider/test/fixtures/mcp-project-isolated.stdin.ndjson +++ /dev/null @@ -1 +0,0 @@ -{"t_ms": 301, "stream": "stdin", "data": {"event": "user", "message": {"content": "Call the phase0 MCP echo tool with text PHASE0_MCP. If it fails report the exact error. No shell."}}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-project-isolated.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-project-isolated.stdout.ndjson deleted file mode 100644 index 632daa6821..0000000000 --- a/plugins/antigravity-provider/test/fixtures/mcp-project-isolated.stdout.ndjson +++ /dev/null @@ -1,31 +0,0 @@ -{"t_ms": 301, "stream": "stdout", "data": {"event": "init", "conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "always-proceed"}}} -{"t_ms": 357, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} -{"t_ms": 3586, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 1, "state": "DONE", "step_type": "agent_response", "duration_seconds": 3.20056914, "usage": {"input_tokens": 11979, "output_tokens": 300, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12279}}}} -{"t_ms": 3587, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 2, "state": "ACTIVE", "step_type": "tool", "tool_name": "view_file", "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/.gemini/antigravity-cli/mcp/phase0/echo.json"}}}}} -{"t_ms": 3587, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 2, "state": "DONE", "step_type": "tool", "tool_name": "view_file", "duration_seconds": 0.003339143, "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/.gemini/antigravity-cli/mcp/phase0/echo.json"}, "output": "1 lines, 184 bytes"}}}} -{"t_ms": 5350, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 3, "state": "DONE", "step_type": "agent_response", "duration_seconds": 1.765191975, "usage": {"input_tokens": 12284, "output_tokens": 61, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12345}}}} -{"t_ms": 5350, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 4, "state": "ACTIVE", "step_type": "tool", "tool_name": "call_mcp_tool", "tool_info": {"name": "call_mcp_tool", "parameters": {"Arguments": "{\"text\":\"PHASE0_MCP\"}", "ServerName": "phase0", "ToolName": "echo"}}}}} -{"t_ms": 5350, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 4, "state": "ERROR", "step_type": "tool", "tool_name": "call_mcp_tool", "duration_seconds": 0.004524751, "tool_info": {"name": "call_mcp_tool", "parameters": {"Arguments": "{\"text\":\"PHASE0_MCP\"}", "ServerName": "phase0", "ToolName": "echo"}, "error": {"type": "TOOL_ERROR", "message": "calling \"tools/call\": [\n {\n \"expected\": \"record\",\n \"code\": \"invalid_type\",\n \"path\": [\n \"params\",\n \"arguments\"\n ],\n \"message\": \"Invalid input: expected record, received string\"\n }\n]"}}}}} -{"t_ms": 7088, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 5, "state": "DONE", "step_type": "agent_response", "duration_seconds": 1.72463283, "usage": {"input_tokens": 12468, "output_tokens": 53, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12521}}}} -{"t_ms": 7088, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 6, "state": "ACTIVE", "step_type": "tool", "tool_name": "call_mcp_tool", "tool_info": {"name": "call_mcp_tool", "parameters": {"Arguments": "null", "ServerName": "phase0", "ToolName": "echo"}}}}} -{"t_ms": 7088, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 6, "state": "ERROR", "step_type": "tool", "tool_name": "call_mcp_tool", "duration_seconds": 0.00450856, "tool_info": {"name": "call_mcp_tool", "parameters": {"Arguments": "null", "ServerName": "phase0", "ToolName": "echo"}, "error": {"type": "TOOL_ERROR", "message": "calling \"tools/call\": [\n {\n \"expected\": \"record\",\n \"code\": \"invalid_type\",\n \"path\": [\n \"params\",\n \"arguments\"\n ],\n \"message\": \"Invalid input: expected record, received string\"\n }\n]"}}}}} -{"t_ms": 8903, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "The call"}}} -{"t_ms": 8979, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " to the `pha"}}} -{"t_ms": 9005, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "se0` MCP `echo` tool fai"}}} -{"t_ms": 9031, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "led with the following e"}}} -{"t_ms": 9056, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "xact error:\n"}}} -{"t_ms": 9082, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "\n"}}} -{"t_ms": 9107, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "```json\ncall"}}} -{"t_ms": 9133, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ing \"tools/call\": [\n {\n"}}} -{"t_ms": 9158, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " \"expected\": \"record\""}}} -{"t_ms": 9185, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": ",\n \"code\""}}} -{"t_ms": 9210, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": ": \"invalid_type\",\n \"path\": [\n "}}} -{"t_ms": 9235, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " \"params\""}}} -{"t_ms": 9261, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": ",\n \"arguments\"\n "}}} -{"t_ms": 9286, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "],\n "}}} -{"t_ms": 9312, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "\"message\": \""}}} -{"t_ms": 9338, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "Invalid input: expected "}}} -{"t_ms": 9363, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "record, rece"}}} -{"t_ms": 9389, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ived string\"\n }\n]\n```"}}} -{"t_ms": 9468, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 7, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 2.395342779, "usage": {"input_tokens": 12644, "output_tokens": 93, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12737}}}} -{"t_ms": 9494, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "status": "SUCCESS", "response": "The call to the `phase0` MCP `echo` tool failed with the following exact error:\n\n```json\ncalling \"tools/call\": [\n {\n \"expected\": \"record\",\n \"code\": \"invalid_type\",\n \"path\": [\n \"params\",\n \"arguments\"\n ],\n \"message\": \"Invalid input: expected record, received string\"\n }\n]\n```\n", "duration_seconds": 9.15918287, "num_turns": 1, "usage": {"input_tokens": 49375, "output_tokens": 507, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 49882}}}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-project.capture.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-project.capture.ndjson deleted file mode 100644 index 041597c746..0000000000 --- a/plugins/antigravity-provider/test/fixtures/mcp-project.capture.ndjson +++ /dev/null @@ -1,2 +0,0 @@ -{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", "", "--disable-slash-commands", "--dangerously-skip-permissions"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} -{"t_ms": 30078, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-project.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-project.stderr.ndjson deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/plugins/antigravity-provider/test/fixtures/mcp-project.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-project.stdin.ndjson deleted file mode 100644 index de5df7cd48..0000000000 --- a/plugins/antigravity-provider/test/fixtures/mcp-project.stdin.ndjson +++ /dev/null @@ -1 +0,0 @@ -{"t_ms": 286, "stream": "stdin", "data": {"event": "user", "message": {"content": "Call the phase0 MCP echo tool with text PHASE0_MCP. Do not use shell. If unavailable say UNAVAILABLE."}}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-project.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-project.stdout.ndjson deleted file mode 100644 index 323648a229..0000000000 --- a/plugins/antigravity-provider/test/fixtures/mcp-project.stdout.ndjson +++ /dev/null @@ -1,15 +0,0 @@ -{"t_ms": 285, "stream": "stdout", "data": {"event": "init", "conversation_id": "89273b5a-77cd-4d4b-ac80-4475b2c751d0", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "always-proceed"}}} -{"t_ms": 339, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "89273b5a-77cd-4d4b-ac80-4475b2c751d0", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} -{"t_ms": 4401, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "89273b5a-77cd-4d4b-ac80-4475b2c751d0", "step_index": 1, "state": "DONE", "step_type": "agent_response", "duration_seconds": 4.075516272, "usage": {"input_tokens": 11933, "output_tokens": 393, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12326}}}} -{"t_ms": 4401, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "89273b5a-77cd-4d4b-ac80-4475b2c751d0", "step_index": 2, "state": "ACTIVE", "step_type": "tool", "tool_name": "view_file", "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/.gemini/antigravity-cli/mcp/phase0/echo.json"}}}}} -{"t_ms": 4427, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "89273b5a-77cd-4d4b-ac80-4475b2c751d0", "step_index": 2, "state": "DONE", "step_type": "tool", "tool_name": "view_file", "duration_seconds": 0.003930268, "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/.gemini/antigravity-cli/mcp/phase0/echo.json"}, "output": "1 lines, 184 bytes"}}}} -{"t_ms": 6440, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "89273b5a-77cd-4d4b-ac80-4475b2c751d0", "step_index": 3, "state": "DONE", "step_type": "agent_response", "duration_seconds": 2.013674566, "usage": {"input_tokens": 12234, "output_tokens": 63, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12297}}}} -{"t_ms": 6441, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "89273b5a-77cd-4d4b-ac80-4475b2c751d0", "step_index": 4, "state": "ACTIVE", "step_type": "tool", "tool_name": "call_mcp_tool", "tool_info": {"name": "call_mcp_tool", "parameters": {"Arguments": "{\"text\":\"PHASE0_MCP\"}", "ServerName": "phase0", "ToolName": "echo"}}}}} -{"t_ms": 6441, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "89273b5a-77cd-4d4b-ac80-4475b2c751d0", "step_index": 4, "state": "ERROR", "step_type": "tool", "tool_name": "call_mcp_tool", "duration_seconds": 0.003539628, "tool_info": {"name": "call_mcp_tool", "parameters": {"Arguments": "{\"text\":\"PHASE0_MCP\"}", "ServerName": "phase0", "ToolName": "echo"}, "error": {"type": "TOOL_ERROR", "message": "calling \"tools/call\": [\n {\n \"expected\": \"record\",\n \"code\": \"invalid_type\",\n \"path\": [\n \"params\",\n \"arguments\"\n ],\n \"message\": \"Invalid input: expected record, received string\"\n }\n]"}}}}} -{"t_ms": 9031, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "89273b5a-77cd-4d4b-ac80-4475b2c751d0", "step_index": 5, "state": "DONE", "step_type": "agent_response", "duration_seconds": 2.5843047, "usage": {"input_tokens": 12420, "output_tokens": 64, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12484}}}} -{"t_ms": 9031, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "89273b5a-77cd-4d4b-ac80-4475b2c751d0", "step_index": 6, "state": "ACTIVE", "step_type": "tool", "tool_name": "call_mcp_tool", "tool_info": {"name": "call_mcp_tool", "parameters": {"Arguments": "{\"text\": \"PHASE0_MCP\"}", "ServerName": "phase0", "ToolName": "echo"}}}}} -{"t_ms": 9031, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "89273b5a-77cd-4d4b-ac80-4475b2c751d0", "step_index": 6, "state": "ERROR", "step_type": "tool", "tool_name": "call_mcp_tool", "duration_seconds": 0.002964553, "tool_info": {"name": "call_mcp_tool", "parameters": {"Arguments": "{\"text\": \"PHASE0_MCP\"}", "ServerName": "phase0", "ToolName": "echo"}, "error": {"type": "TOOL_ERROR", "message": "calling \"tools/call\": [\n {\n \"expected\": \"record\",\n \"code\": \"invalid_type\",\n \"path\": [\n \"params\",\n \"arguments\"\n ],\n \"message\": \"Invalid input: expected record, received string\"\n }\n]"}}}}} -{"t_ms": 24041, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "89273b5a-77cd-4d4b-ac80-4475b2c751d0", "step_index": 7, "state": "DONE", "step_type": "error_message", "duration_seconds": 0}}} -{"t_ms": 29423, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "89273b5a-77cd-4d4b-ac80-4475b2c751d0", "step_index": 8, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "UNAVAILABLE"}}} -{"t_ms": 29980, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "89273b5a-77cd-4d4b-ac80-4475b2c751d0", "step_index": 8, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 1.922531311, "usage": {"input_tokens": 12607, "output_tokens": 2, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12609}}}} -{"t_ms": 29980, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "89273b5a-77cd-4d4b-ac80-4475b2c751d0", "status": "ERROR", "response": "UNAVAILABLE\n", "error": "API error (attempt 1): Error 502, Message: , Status: 502 Bad Gateway, Details: []", "duration_seconds": 29.671957293, "num_turns": 1, "usage": {"input_tokens": 49194, "output_tokens": 522, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 49716}}}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-second-add-dir.capture.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-second-add-dir.capture.ndjson deleted file mode 100644 index d42fd1a128..0000000000 --- a/plugins/antigravity-provider/test/fixtures/mcp-second-add-dir.capture.ndjson +++ /dev/null @@ -1,2 +0,0 @@ -{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--disable-slash-commands", "--dangerously-skip-permissions", "--add-dir", "", "--add-dir", "/owned"], "cwd": "", "home": "/mcp-second-add-dir-home", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} -{"t_ms": 18531, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-second-add-dir.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-second-add-dir.stderr.ndjson deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/plugins/antigravity-provider/test/fixtures/mcp-second-add-dir.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-second-add-dir.stdin.ndjson deleted file mode 100644 index eeddbbd260..0000000000 --- a/plugins/antigravity-provider/test/fixtures/mcp-second-add-dir.stdin.ndjson +++ /dev/null @@ -1 +0,0 @@ -{"t_ms": 308, "stream": "stdin", "data": {"event": "user", "message": {"content": "First call the phase2 MCP echo tool with text PHASE2_DISCOVERY. If unavailable say MCP_UNAVAILABLE. Then use shell pwd, read input.txt and write output.txt containing PHASE2_WRITE. Use the workspace default directory, without me supplying absolute paths. Report the exact paths used."}}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-second-add-dir.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-second-add-dir.stdout.ndjson deleted file mode 100644 index 274627ad6b..0000000000 --- a/plugins/antigravity-provider/test/fixtures/mcp-second-add-dir.stdout.ndjson +++ /dev/null @@ -1,71 +0,0 @@ -{"t_ms": 308, "stream": "stdout", "data": {"event": "init", "conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "always-proceed"}}} -{"t_ms": 386, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} -{"t_ms": 4340, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 1, "state": "DONE", "step_type": "agent_response", "duration_seconds": 3.94876824, "usage": {"input_tokens": 11996, "output_tokens": 440, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12436}}}} -{"t_ms": 4340, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 2, "state": "ACTIVE", "step_type": "tool", "tool_name": "view_file", "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/mcp-second-add-dir-home/.gemini/antigravity-cli/mcp/phase2/echo.json"}}}}} -{"t_ms": 4340, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 2, "state": "DONE", "step_type": "tool", "tool_name": "view_file", "duration_seconds": 0.003707182, "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/mcp-second-add-dir-home/.gemini/antigravity-cli/mcp/phase2/echo.json"}, "output": "1 lines, 184 bytes"}}}} -{"t_ms": 6080, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 3, "state": "DONE", "step_type": "agent_response", "duration_seconds": 1.730381429, "usage": {"input_tokens": 12306, "output_tokens": 64, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12370}}}} -{"t_ms": 6080, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 4, "state": "ACTIVE", "step_type": "tool", "tool_name": "call_mcp_tool", "tool_info": {"name": "call_mcp_tool", "parameters": {"Arguments": "{\"text\":\"PHASE2_DISCOVERY\"}", "ServerName": "phase2", "ToolName": "echo"}}}}} -{"t_ms": 6080, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 4, "state": "ERROR", "step_type": "tool", "tool_name": "call_mcp_tool", "duration_seconds": 0.005173122, "tool_info": {"name": "call_mcp_tool", "parameters": {"Arguments": "{\"text\":\"PHASE2_DISCOVERY\"}", "ServerName": "phase2", "ToolName": "echo"}, "error": {"type": "TOOL_ERROR", "message": "calling \"tools/call\": [\n {\n \"expected\": \"record\",\n \"code\": \"invalid_type\",\n \"path\": [\n \"params\",\n \"arguments\"\n ],\n \"message\": \"Invalid input: expected record, received string\"\n }\n]"}}}}} -{"t_ms": 7982, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 5, "state": "DONE", "step_type": "agent_response", "duration_seconds": 1.901210495, "usage": {"input_tokens": 12493, "output_tokens": 68, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12561}}}} -{"t_ms": 7982, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 6, "state": "ACTIVE", "step_type": "tool", "tool_name": "run_command", "tool_info": {"name": "run_command", "parameters": {"CommandLine": "pwd"}}}}} -{"t_ms": 7982, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 6, "state": "DONE", "step_type": "tool", "tool_name": "run_command", "duration_seconds": 0.006768796, "tool_info": {"name": "run_command", "parameters": {"CommandLine": "pwd"}, "output": "/owned\r\n"}}}} -{"t_ms": 9669, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 7, "state": "DONE", "step_type": "agent_response", "duration_seconds": 1.692085483, "usage": {"input_tokens": 12626, "output_tokens": 48, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12674}}}} -{"t_ms": 9669, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 8, "state": "ACTIVE", "step_type": "tool", "tool_name": "view_file", "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/owned/input.txt"}}}}} -{"t_ms": 9669, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 8, "state": "ERROR", "step_type": "tool", "tool_name": "view_file", "duration_seconds": 0.001462294, "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/owned/input.txt"}, "error": {"type": "TOOL_ERROR", "message": "declaring permissions: cortex tool view_file: convert tool call for permissions: model output error: invalid tool call error (invalid_args) failed to read file: stat /owned/input.txt: no such file or directory"}}}}} -{"t_ms": 11257, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 9, "state": "DONE", "step_type": "agent_response", "duration_seconds": 1.561062696, "usage": {"input_tokens": 12788, "output_tokens": 51, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12839}}}} -{"t_ms": 11257, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 10, "state": "ACTIVE", "step_type": "tool", "tool_name": "view_file", "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/input.txt"}}}}} -{"t_ms": 11257, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 10, "state": "DONE", "step_type": "tool", "tool_name": "view_file", "duration_seconds": 0.002866119, "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/input.txt"}, "output": "1 lines, 23 bytes"}}}} -{"t_ms": 13169, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 11, "state": "DONE", "step_type": "agent_response", "duration_seconds": 1.9176663440000001, "usage": {"input_tokens": 13010, "output_tokens": 84, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 13094}}}} -{"t_ms": 13170, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 12, "state": "ACTIVE", "step_type": "tool", "tool_name": "write_to_file", "tool_info": {"name": "write_to_file", "parameters": {"TargetFile": "/output.txt"}}}}} -{"t_ms": 13170, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 12, "state": "DONE", "step_type": "tool", "tool_name": "write_to_file", "duration_seconds": 0.003297375, "tool_info": {"name": "write_to_file", "parameters": {"TargetFile": "/output.txt"}}}}} -{"t_ms": 15284, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 13, "state": "DONE", "step_type": "agent_response", "duration_seconds": 2.118805123, "usage": {"input_tokens": 13182, "output_tokens": 84, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 13266}}}} -{"t_ms": 15284, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 14, "state": "ACTIVE", "step_type": "tool", "tool_name": "write_to_file", "tool_info": {"name": "write_to_file", "parameters": {"TargetFile": "/owned/output.txt"}}}}} -{"t_ms": 15284, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 14, "state": "DONE", "step_type": "tool", "tool_name": "write_to_file", "duration_seconds": 0.003384669, "tool_info": {"name": "write_to_file", "parameters": {"TargetFile": "/owned/output.txt"}}}}} -{"t_ms": 16695, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "### MCP"}}} -{"t_ms": 16797, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " Tool Execut"}}} -{"t_ms": 16823, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ion\n- Calling the `phase"}}} -{"t_ms": 16848, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "2` MCP `echo` tool faile"}}} -{"t_ms": 16874, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "d with an"}}} -{"t_ms": 17102, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " argument validation error from the "}}} -{"t_ms": 17128, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "server schem"}}} -{"t_ms": 17153, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "a interface:\n `MCP_UNAV"}}} -{"t_ms": 17178, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "AILABLE` (er"}}} -{"t_ms": 17204, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ror: `Invalid input: expected record"}}} -{"t_ms": 17229, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": ", received s"}}} -{"t_ms": 17255, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "tring`)\n\n---\n\n### Shell "}}} -{"t_ms": 17281, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "`pwd` & Workspace File O"}}} -{"t_ms": 17306, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "perations\n- "}}} -{"t_ms": 17331, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "**Current Working Directory (`pwd`):"}}} -{"t_ms": 17356, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "** `/tmp/agy"}}} -{"t_ms": 17382, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "-p2/owned`\n- **Read file"}}} -{"t_ms": 17407, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": ":** [input.txt](file:///w"}}} -{"t_ms": 17433, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "orkspace/inp"}}} -{"t_ms": 17459, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ut.txt) (Content: `PHASE"}}} -{"t_ms": 17484, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "2_WORKSPACE_"}}} -{"t_ms": 17509, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "MARK"}}} -{"t_ms": 17535, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ER`)\n- **Written file:**"}}} -{"t_ms": 17560, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " [output.txt"}}} -{"t_ms": 17586, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "](file:///wor"}}} -{"t_ms": 17611, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "kspace/outpu"}}} -{"t_ms": 17637, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "t"}}} -{"t_ms": 17738, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": ".txt) (and m"}}} -{"t_ms": 17764, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "irrored to [output.txt]("}}} -{"t_ms": 17789, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "file:///tmp/"}}} -{"t_ms": 17814, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "agy-p2/owned/output.txt)"}}} -{"t_ms": 17840, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": ") containing "}}} -{"t_ms": 17941, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "`PHASE2_WRITE`\n\n### Exac"}}} -{"t_ms": 17966, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "t Paths Used\n- Default C"}}} -{"t_ms": 17992, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ommand Worki"}}} -{"t_ms": 18017, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ng Directory: `/tmp/agy-"}}} -{"t_ms": 18043, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "p2/owned"}}} -{"t_ms": 18069, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "`\n- Workspac"}}} -{"t_ms": 18095, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "e Read Path: `/tmp/agy-p"}}} -{"t_ms": 18120, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "2/workspace/"}}} -{"t_ms": 18145, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "input.txt`\n- Output Writ"}}} -{"t_ms": 18171, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ten"}}} -{"t_ms": 18196, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " Paths:\n - `"}}} -{"t_ms": 18222, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "/workspace/o"}}} -{"t_ms": 18247, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "utput.txt`\n - `/tmp"}}} -{"t_ms": 18273, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "/agy-p2/owned/output.txt`"}}} -{"t_ms": 18450, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 3.1541972400000002, "usage": {"input_tokens": 13354, "output_tokens": 262, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 13616}}}} -{"t_ms": 18450, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "status": "SUCCESS", "response": "### MCP Tool Execution\n- Calling the `phase2` MCP `echo` tool failed with an argument validation error from the server schema interface:\n `MCP_UNAVAILABLE` (error: `Invalid input: expected record, received string`)\n\n---\n\n### Shell `pwd` & Workspace File Operations\n- **Current Working Directory (`pwd`):** `/owned`\n- **Read file:** [input.txt](file:///input.txt) (Content: `PHASE2_WORKSPACE_MARKER`)\n- **Written file:** [output.txt](file:///output.txt) (and mirrored to [output.txt](file:///owned/output.txt)) containing `PHASE2_WRITE`\n\n### Exact Paths Used\n- Default Command Working Directory: `/owned`\n- Workspace Read Path: `/input.txt`\n- Output Written Paths:\n - `/output.txt`\n - `/owned/output.txt`\n", "duration_seconds": 18.127914828, "num_turns": 1, "usage": {"input_tokens": 101755, "output_tokens": 1101, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 102856}}}} diff --git a/plugins/antigravity-provider/test/fixtures/model-switch-invalid.capture.ndjson b/plugins/antigravity-provider/test/fixtures/model-switch-invalid.capture.ndjson deleted file mode 100644 index d9a08ce530..0000000000 --- a/plugins/antigravity-provider/test/fixtures/model-switch-invalid.capture.ndjson +++ /dev/null @@ -1,3 +0,0 @@ -{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", "", "--conversation", "b966b16a-caf4-448a-bb24-0e34956022b0", "--model", "gemini-3.1-flash-lite-preview"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} -{"t_ms": 4004, "stream": "capture", "data": {"timeout_waiting_for": "init", "seconds": 4}} -{"t_ms": 4005, "stream": "capture", "data": {"exit_code": 1}} diff --git a/plugins/antigravity-provider/test/fixtures/model-switch-invalid.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/model-switch-invalid.stderr.ndjson deleted file mode 100644 index 244d1ac13e..0000000000 --- a/plugins/antigravity-provider/test/fixtures/model-switch-invalid.stderr.ndjson +++ /dev/null @@ -1,13 +0,0 @@ -{"t_ms": 296, "stream": "stderr", "data": "error: invalid model selection (--model \"gemini-3.1-flash-lite-preview\" --effort \"\"): model gemini-3.1-flash-lite-preview is not recognized as a known model or custom model in settings"} -{"t_ms": 296, "stream": "stderr", "data": "Available models:"} -{"t_ms": 296, "stream": "stderr", "data": " Gemini 3.8 Flash (High)"} -{"t_ms": 296, "stream": "stderr", "data": " Gemini 3.8 Flash (Medium)"} -{"t_ms": 296, "stream": "stderr", "data": " Gemini 3.8 Flash (Low)"} -{"t_ms": 296, "stream": "stderr", "data": " Gemini 3.7 Flash (High)"} -{"t_ms": 296, "stream": "stderr", "data": " Gemini 3.7 Flash (Medium)"} -{"t_ms": 296, "stream": "stderr", "data": " Gemini 3.7 Flash (Low)"} -{"t_ms": 296, "stream": "stderr", "data": " Gemini 3.6 Flash (High)"} -{"t_ms": 296, "stream": "stderr", "data": " Gemini 3.6 Flash (Medium)"} -{"t_ms": 296, "stream": "stderr", "data": " Gemini 3.6 Flash (Low)"} -{"t_ms": 297, "stream": "stderr", "data": " Gemini 3.1 Pro (High)"} -{"t_ms": 297, "stream": "stderr", "data": " Gemini 3.1 Pro (Low)"} diff --git a/plugins/antigravity-provider/test/fixtures/model-switch-invalid.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/model-switch-invalid.stdin.ndjson deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/plugins/antigravity-provider/test/fixtures/model-switch-invalid.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/model-switch-invalid.stdout.ndjson deleted file mode 100644 index 1ad10e7494..0000000000 --- a/plugins/antigravity-provider/test/fixtures/model-switch-invalid.stdout.ndjson +++ /dev/null @@ -1 +0,0 @@ -{"t_ms": 297, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "", "status": "ERROR", "response": "", "error": "invalid model selection (--model \"gemini-3.1-flash-lite-preview\" --effort \"\"): model gemini-3.1-flash-lite-preview is not recognized as a known model or custom model in settings\nAvailable models:\n Gemini 3.8 Flash (High)\n Gemini 3.8 Flash (Medium)\n Gemini 3.8 Flash (Low)\n Gemini 3.7 Flash (High)\n Gemini 3.7 Flash (Medium)\n Gemini 3.7 Flash (Low)\n Gemini 3.6 Flash (High)\n Gemini 3.6 Flash (Medium)\n Gemini 3.6 Flash (Low)\n Gemini 3.1 Pro (High)\n Gemini 3.1 Pro (Low)", "duration_seconds": 0, "num_turns": 0, "usage": {"input_tokens": 0, "output_tokens": 0, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 0}}}} diff --git a/plugins/antigravity-provider/test/fixtures/model-switch-network.capture.ndjson b/plugins/antigravity-provider/test/fixtures/model-switch-network.capture.ndjson deleted file mode 100644 index 243104c416..0000000000 --- a/plugins/antigravity-provider/test/fixtures/model-switch-network.capture.ndjson +++ /dev/null @@ -1,3 +0,0 @@ -{"t_ms": 1, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--conversation", "b966b16a-caf4-448a-bb24-0e34956022b0", "--model", "gemini-3.8-flash-low"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} -{"t_ms": 2405, "stream": "capture", "data": {"translator_http": {"request": 6, "path": "/v1beta/models/gemini-3.8-flash:streamGenerateContent?alt=sse", "model": "google/gemini-3.7-flash", "roles": ["system", "user", "assistant", "user", "assistant", "user", "user", "assistant", "user", "user", "assistant", "user", "user"], "tools": ["view_file", "run_command", "manage_task", "send_message", "schedule", "invoke_subagent", "define_subagent", "manage_subagents", "write_to_file", "replace_file_content", "generate_image", "read_url_content", "search_web", "ask_question"], "usage": {"prompt_tokens": 12105, "completion_tokens": 12, "total_tokens": 12117, "cost": 0.00912375, "is_byok": false, "prompt_tokens_details": {"cached_tokens": 0, "cache_write_tokens": 0, "audio_tokens": 0, "video_tokens": 0}, "cost_details": {"upstream_inference_cost": 0.00912375, "upstream_inference_prompt_cost": 0.00907875, "upstream_inference_completions_cost": 4.5e-05}, "completion_tokens_details": {"reasoning_tokens": 0, "image_tokens": 0, "audio_tokens": 0}}, "toolCalls": [], "status": 200}}} -{"t_ms": 2572, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/model-switch-network.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/model-switch-network.stderr.ndjson deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/plugins/antigravity-provider/test/fixtures/model-switch-network.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/model-switch-network.stdin.ndjson deleted file mode 100644 index eb33e7913d..0000000000 --- a/plugins/antigravity-provider/test/fixtures/model-switch-network.stdin.ndjson +++ /dev/null @@ -1 +0,0 @@ -{"t_ms": 439, "stream": "stdin", "data": {"event": "user", "message": {"content": "What secret word did I give you? No tools."}}} diff --git a/plugins/antigravity-provider/test/fixtures/model-switch-network.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/model-switch-network.stdout.ndjson deleted file mode 100644 index e1f755f3f2..0000000000 --- a/plugins/antigravity-provider/test/fixtures/model-switch-network.stdout.ndjson +++ /dev/null @@ -1,8 +0,0 @@ -{"t_ms": 439, "stream": "stdout", "data": {"event": "init", "conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "init": {"model": "gemini-3.8-flash-low", "cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "request-review"}}} -{"t_ms": 560, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 10, "state": "DONE", "step_type": "user_input"}}} -{"t_ms": 560, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 11, "state": "DONE", "step_type": "system_message", "duration_seconds": 9.9025e-05}}} -{"t_ms": 2045, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 12, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "The secret"}}} -{"t_ms": 2070, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 12, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " word you ga"}}} -{"t_ms": 2096, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 12, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ve me is **COBALT**."}}} -{"t_ms": 2405, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 12, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 1.873175086, "usage": {"input_tokens": 12105, "output_tokens": 12, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12117}}}} -{"t_ms": 2405, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "status": "SUCCESS", "response": "The secret word you gave me is **COBALT**.\n", "duration_seconds": 282.68791325, "num_turns": 5, "usage": {"input_tokens": 58633, "output_tokens": 505, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 59138}}}} diff --git a/plugins/antigravity-provider/test/fixtures/model-switch-resume.capture.ndjson b/plugins/antigravity-provider/test/fixtures/model-switch-resume.capture.ndjson deleted file mode 100644 index 961223783a..0000000000 --- a/plugins/antigravity-provider/test/fixtures/model-switch-resume.capture.ndjson +++ /dev/null @@ -1,3 +0,0 @@ -{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", "", "--conversation", "b966b16a-caf4-448a-bb24-0e34956022b0", "--model", "gemini-3.1-flash-high"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} -{"t_ms": 4004, "stream": "capture", "data": {"timeout_waiting_for": "init", "seconds": 4}} -{"t_ms": 4004, "stream": "capture", "data": {"exit_code": 1}} diff --git a/plugins/antigravity-provider/test/fixtures/model-switch-resume.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/model-switch-resume.stderr.ndjson deleted file mode 100644 index 2666337e77..0000000000 --- a/plugins/antigravity-provider/test/fixtures/model-switch-resume.stderr.ndjson +++ /dev/null @@ -1,13 +0,0 @@ -{"t_ms": 239, "stream": "stderr", "data": "error: invalid model selection (--model \"gemini-3.1-flash-high\" --effort \"\"): model gemini-3.1-flash-high is not recognized as a known model or custom model in settings"} -{"t_ms": 239, "stream": "stderr", "data": "Available models:"} -{"t_ms": 239, "stream": "stderr", "data": " Gemini 3.8 Flash (High)"} -{"t_ms": 239, "stream": "stderr", "data": " Gemini 3.8 Flash (Medium)"} -{"t_ms": 239, "stream": "stderr", "data": " Gemini 3.8 Flash (Low)"} -{"t_ms": 239, "stream": "stderr", "data": " Gemini 3.7 Flash (High)"} -{"t_ms": 239, "stream": "stderr", "data": " Gemini 3.7 Flash (Medium)"} -{"t_ms": 239, "stream": "stderr", "data": " Gemini 3.7 Flash (Low)"} -{"t_ms": 239, "stream": "stderr", "data": " Gemini 3.6 Flash (High)"} -{"t_ms": 239, "stream": "stderr", "data": " Gemini 3.6 Flash (Medium)"} -{"t_ms": 239, "stream": "stderr", "data": " Gemini 3.6 Flash (Low)"} -{"t_ms": 239, "stream": "stderr", "data": " Gemini 3.1 Pro (High)"} -{"t_ms": 239, "stream": "stderr", "data": " Gemini 3.1 Pro (Low)"} diff --git a/plugins/antigravity-provider/test/fixtures/model-switch-resume.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/model-switch-resume.stdin.ndjson deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/plugins/antigravity-provider/test/fixtures/model-switch-resume.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/model-switch-resume.stdout.ndjson deleted file mode 100644 index 3556e4c951..0000000000 --- a/plugins/antigravity-provider/test/fixtures/model-switch-resume.stdout.ndjson +++ /dev/null @@ -1 +0,0 @@ -{"t_ms": 239, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "", "status": "ERROR", "response": "", "error": "invalid model selection (--model \"gemini-3.1-flash-high\" --effort \"\"): model gemini-3.1-flash-high is not recognized as a known model or custom model in settings\nAvailable models:\n Gemini 3.8 Flash (High)\n Gemini 3.8 Flash (Medium)\n Gemini 3.8 Flash (Low)\n Gemini 3.7 Flash (High)\n Gemini 3.7 Flash (Medium)\n Gemini 3.7 Flash (Low)\n Gemini 3.6 Flash (High)\n Gemini 3.6 Flash (Medium)\n Gemini 3.6 Flash (Low)\n Gemini 3.1 Pro (High)\n Gemini 3.1 Pro (Low)", "duration_seconds": 0, "num_turns": 0, "usage": {"input_tokens": 0, "output_tokens": 0, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 0}}}} diff --git a/plugins/antigravity-provider/test/fixtures/model-switch-valid.capture.ndjson b/plugins/antigravity-provider/test/fixtures/model-switch-valid.capture.ndjson deleted file mode 100644 index 9d44fc439e..0000000000 --- a/plugins/antigravity-provider/test/fixtures/model-switch-valid.capture.ndjson +++ /dev/null @@ -1,2 +0,0 @@ -{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--conversation", "b966b16a-caf4-448a-bb24-0e34956022b0", "--model", "gemini-3.8-flash-low"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} -{"t_ms": 4608, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/model-switch-valid.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/model-switch-valid.stderr.ndjson deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/plugins/antigravity-provider/test/fixtures/model-switch-valid.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/model-switch-valid.stdin.ndjson deleted file mode 100644 index f1fb9508ac..0000000000 --- a/plugins/antigravity-provider/test/fixtures/model-switch-valid.stdin.ndjson +++ /dev/null @@ -1 +0,0 @@ -{"t_ms": 296, "stream": "stdin", "data": {"event": "user", "message": {"content": "What secret word did I give you? No tools."}}} diff --git a/plugins/antigravity-provider/test/fixtures/model-switch-valid.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/model-switch-valid.stdout.ndjson deleted file mode 100644 index 70dbf755eb..0000000000 --- a/plugins/antigravity-provider/test/fixtures/model-switch-valid.stdout.ndjson +++ /dev/null @@ -1,7 +0,0 @@ -{"t_ms": 296, "stream": "stdout", "data": {"event": "init", "conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "init": {"model": "gemini-3.8-flash-low", "cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "request-review"}}} -{"t_ms": 342, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 7, "state": "DONE", "step_type": "user_input"}}} -{"t_ms": 342, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 8, "state": "DONE", "step_type": "system_message", "duration_seconds": 4.783e-05}}} -{"t_ms": 3891, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "The secret"}}} -{"t_ms": 4018, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " word you gave me is **COBALT**."}}} -{"t_ms": 4473, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 9, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 4.143724393, "usage": {"input_tokens": 11846, "output_tokens": 12, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11858}}}} -{"t_ms": 4499, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "status": "SUCCESS", "response": "The secret word you gave me is **COBALT**.\n", "duration_seconds": 129.476794036, "num_turns": 4, "usage": {"input_tokens": 46528, "output_tokens": 493, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 47021}}}} diff --git a/plugins/antigravity-provider/test/fixtures/models-api-key.capture.ndjson b/plugins/antigravity-provider/test/fixtures/models-api-key.capture.ndjson deleted file mode 100644 index 2db201ce59..0000000000 --- a/plugins/antigravity-provider/test/fixtures/models-api-key.capture.ndjson +++ /dev/null @@ -1,2 +0,0 @@ -{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "models"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} -{"t_ms": 360, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/models-api-key.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/models-api-key.stderr.ndjson deleted file mode 100644 index 31ed588249..0000000000 --- a/plugins/antigravity-provider/test/fixtures/models-api-key.stderr.ndjson +++ /dev/null @@ -1 +0,0 @@ -{"t_ms": 247, "stream": "stderr", "data": "Fetching available models..."} diff --git a/plugins/antigravity-provider/test/fixtures/models-api-key.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/models-api-key.stdin.ndjson deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/plugins/antigravity-provider/test/fixtures/models-api-key.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/models-api-key.stdout.ndjson deleted file mode 100644 index ece429fc4b..0000000000 --- a/plugins/antigravity-provider/test/fixtures/models-api-key.stdout.ndjson +++ /dev/null @@ -1,11 +0,0 @@ -{"t_ms": 248, "stream": "stdout", "data": "gemini-3.8-flash-high\tGemini 3.8 Flash (High)"} -{"t_ms": 248, "stream": "stdout", "data": "gemini-3.8-flash-medium\tGemini 3.8 Flash (Medium)"} -{"t_ms": 248, "stream": "stdout", "data": "gemini-3.8-flash-low\tGemini 3.8 Flash (Low)"} -{"t_ms": 248, "stream": "stdout", "data": "gemini-3.7-flash-high\tGemini 3.7 Flash (High)"} -{"t_ms": 248, "stream": "stdout", "data": "gemini-3.7-flash-medium\tGemini 3.7 Flash (Medium)"} -{"t_ms": 248, "stream": "stdout", "data": "gemini-3.7-flash-low\tGemini 3.7 Flash (Low)"} -{"t_ms": 248, "stream": "stdout", "data": "gemini-3.6-flash-high\tGemini 3.6 Flash (High)"} -{"t_ms": 248, "stream": "stdout", "data": "gemini-3.6-flash-medium\tGemini 3.6 Flash (Medium)"} -{"t_ms": 248, "stream": "stdout", "data": "gemini-3.6-flash-low\tGemini 3.6 Flash (Low)"} -{"t_ms": 248, "stream": "stdout", "data": "gemini-3.1-pro-high\tGemini 3.1 Pro (High)"} -{"t_ms": 248, "stream": "stdout", "data": "gemini-3.1-pro-low\tGemini 3.1 Pro (Low)"} diff --git a/plugins/antigravity-provider/test/fixtures/models-unauthenticated.capture.ndjson b/plugins/antigravity-provider/test/fixtures/models-unauthenticated.capture.ndjson deleted file mode 100644 index b91f7dfb82..0000000000 --- a/plugins/antigravity-provider/test/fixtures/models-unauthenticated.capture.ndjson +++ /dev/null @@ -1,2 +0,0 @@ -{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "models"], "cwd": "", "home": "", "settings": null, "env": {}}} -{"t_ms": 567, "stream": "capture", "data": {"exit_code": 1}} diff --git a/plugins/antigravity-provider/test/fixtures/models-unauthenticated.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/models-unauthenticated.stderr.ndjson deleted file mode 100644 index 0bcbc4745c..0000000000 --- a/plugins/antigravity-provider/test/fixtures/models-unauthenticated.stderr.ndjson +++ /dev/null @@ -1,2 +0,0 @@ -{"t_ms": 404, "stream": "stderr", "data": "Fetching available models..."} -{"t_ms": 404, "stream": "stderr", "data": "Error: Please sign in to view available models. Launch the CLI without arguments to sign in."} diff --git a/plugins/antigravity-provider/test/fixtures/models-unauthenticated.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/models-unauthenticated.stdin.ndjson deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/plugins/antigravity-provider/test/fixtures/models-unauthenticated.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/models-unauthenticated.stdout.ndjson deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/plugins/antigravity-provider/test/fixtures/multi-turn-reasoning.capture.ndjson b/plugins/antigravity-provider/test/fixtures/multi-turn-reasoning.capture.ndjson deleted file mode 100644 index 56e04c0abe..0000000000 --- a/plugins/antigravity-provider/test/fixtures/multi-turn-reasoning.capture.ndjson +++ /dev/null @@ -1,2 +0,0 @@ -{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", "", "--disable-slash-commands"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} -{"t_ms": 6732, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/multi-turn-reasoning.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/multi-turn-reasoning.stderr.ndjson deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/plugins/antigravity-provider/test/fixtures/multi-turn-reasoning.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/multi-turn-reasoning.stdin.ndjson deleted file mode 100644 index 4210a3f1b6..0000000000 --- a/plugins/antigravity-provider/test/fixtures/multi-turn-reasoning.stdin.ndjson +++ /dev/null @@ -1,2 +0,0 @@ -{"t_ms": 304, "stream": "stdin", "data": {"event": "user", "message": {"role": "user", "content": "Remember the secret word COBALT. Solve 17*23 with reasoning, then reply with the answer. No tools."}}} -{"t_ms": 4752, "stream": "stdin", "data": {"event": "user", "message": {"role": "user", "content": "What secret word did I give you? Reply with only that word. No tools."}}} diff --git a/plugins/antigravity-provider/test/fixtures/multi-turn-reasoning.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/multi-turn-reasoning.stdout.ndjson deleted file mode 100644 index dd2a346d75..0000000000 --- a/plugins/antigravity-provider/test/fixtures/multi-turn-reasoning.stdout.ndjson +++ /dev/null @@ -1,51 +0,0 @@ -{"t_ms": 304, "stream": "stdout", "data": {"event": "init", "conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "request-review"}}} -{"t_ms": 332, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} -{"t_ms": 3128, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "I have noted"}}} -{"t_ms": 3153, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " and remembe"}}} -{"t_ms": 3178, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "red the secret word **CO"}}} -{"t_ms": 3203, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "BALT**.\n\n###"}}} -{"t_ms": 3229, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " Reasoning"}}} -{"t_ms": 3280, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " for $17 \\ti"}}} -{"t_ms": 3306, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "mes 23$:\n\nWe can solve $"}}} -{"t_ms": 3332, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "17 \\times 23$ using the "}}} -{"t_ms": 3357, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "difference o"}}} -{"t_ms": 3382, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "f"}}} -{"t_ms": 3459, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " squares ide"}}} -{"t_ms": 3484, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ntity, $(a - b)(a + b) ="}}} -{"t_ms": 3510, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " a^2 - b^2$:\n\n1. Rewrite"}}} -{"t_ms": 3535, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " the"}}} -{"t_ms": 3688, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " terms centered around 20:\n $$17 ="}}} -{"t_ms": 3713, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " 20 - 3$$\n "}}} -{"t_ms": 3738, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " $$23"}}} -{"t_ms": 3814, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " = 20 + 3$$\n\n2. Apply th"}}} -{"t_ms": 3840, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "e difference"}}} -{"t_ms": 3865, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " of squares formula:\n "}}} -{"t_ms": 3891, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "$$17 \\times "}}} -{"t_ms": 3917, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "23 = (20 - 3)(20 + 3) = 2"}}} -{"t_ms": 3942, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "0^2 - 3"}}} -{"t_ms": 4069, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "^2$$\n\n3. Cal"}}} -{"t_ms": 4094, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "culate the squares:\n $"}}} -{"t_ms": 4119, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "$20^2 = 400$"}}} -{"t_ms": 4145, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "$\n "}}} -{"t_ms": 4170, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "$$3^2 = 9$$\n"}}} -{"t_ms": 4221, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "\n4. Subtract"}}} -{"t_ms": 4246, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": ":\n $$400 -"}}} -{"t_ms": 4271, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " 9 = 3"}}} -{"t_ms": 4323, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "91$$\n\n*(Alte"}}} -{"t_ms": 4348, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "rnatively, v"}}} -{"t_ms": 4398, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ia partial p"}}} -{"t_ms": 4424, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "roducts: $17"}}} -{"t_ms": 4474, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " \\times 20 ="}}} -{"t_ms": 4499, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " 340$, and $"}}} -{"t_ms": 4550, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "17 \\times 3 "}}} -{"t_ms": 4575, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "= 51$; then "}}} -{"t_ms": 4600, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "$340 + 51 = "}}} -{"t_ms": 4650, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "391$.)*\n\n---"}}} -{"t_ms": 4675, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "\n\n**Answer:*"}}} -{"t_ms": 4726, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "* **391**"}}} -{"t_ms": 4752, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 4.392588837, "usage": {"input_tokens": 11263, "output_tokens": 466, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11729}}}} -{"t_ms": 4752, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "status": "SUCCESS", "response": "I have noted and remembered the secret word **COBALT**.\n\n### Reasoning for $17 \\times 23$:\n\nWe can solve $17 \\times 23$ using the difference of squares identity, $(a - b)(a + b) = a^2 - b^2$:\n\n1. Rewrite the terms centered around 20:\n $$17 = 20 - 3$$\n $$23 = 20 + 3$$\n\n2. Apply the difference of squares formula:\n $$17 \\times 23 = (20 - 3)(20 + 3) = 20^2 - 3^2$$\n\n3. Calculate the squares:\n $$20^2 = 400$$\n $$3^2 = 9$$\n\n4. Subtract:\n $$400 - 9 = 391$$\n\n*(Alternatively, via partial products: $17 \\times 20 = 340$, and $17 \\times 3 = 51$; then $340 + 51 = 391$.)*\n\n---\n\n**Answer:** **391**\n", "duration_seconds": 4.414417492, "num_turns": 1, "usage": {"input_tokens": 11263, "output_tokens": 466, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11729}}}} -{"t_ms": 4785, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 2, "state": "DONE", "step_type": "user_input"}}} -{"t_ms": 6345, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "COBALT"}}} -{"t_ms": 6573, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 3, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 1.784280065, "usage": {"input_tokens": 11604, "output_tokens": 3, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11607}}}} -{"t_ms": 6573, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "status": "SUCCESS", "response": "COBALT\n", "duration_seconds": 6.242840924, "num_turns": 2, "usage": {"input_tokens": 22867, "output_tokens": 469, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 23336}}}} diff --git a/plugins/antigravity-provider/test/fixtures/not-signed-in.capture.ndjson b/plugins/antigravity-provider/test/fixtures/not-signed-in.capture.ndjson deleted file mode 100644 index a77dc1e4ee..0000000000 --- a/plugins/antigravity-provider/test/fixtures/not-signed-in.capture.ndjson +++ /dev/null @@ -1,2 +0,0 @@ -{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json"], "cwd": "", "home": "", "settings": null, "env": {}}} -{"t_ms": 520, "stream": "capture", "data": {"exit_code": 1}} diff --git a/plugins/antigravity-provider/test/fixtures/not-signed-in.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/not-signed-in.stderr.ndjson deleted file mode 100644 index 9304e8b2ee..0000000000 --- a/plugins/antigravity-provider/test/fixtures/not-signed-in.stderr.ndjson +++ /dev/null @@ -1,2 +0,0 @@ -{"t_ms": 360, "stream": "stderr", "data": "Error: authentication required. Run 'agy' to log in, then retry."} -{"t_ms": 360, "stream": "stderr", "data": "error: authentication failed or timed out"} diff --git a/plugins/antigravity-provider/test/fixtures/not-signed-in.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/not-signed-in.stdin.ndjson deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/plugins/antigravity-provider/test/fixtures/not-signed-in.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/not-signed-in.stdout.ndjson deleted file mode 100644 index 760f2dbe68..0000000000 --- a/plugins/antigravity-provider/test/fixtures/not-signed-in.stdout.ndjson +++ /dev/null @@ -1 +0,0 @@ -{"t_ms": 360, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "", "status": "ERROR", "response": "", "error": "authentication failed or timed out", "duration_seconds": 0, "num_turns": 0, "usage": {"input_tokens": 0, "output_tokens": 0, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 0}}}} diff --git a/plugins/antigravity-provider/test/fixtures/parity-inspection.ndjson b/plugins/antigravity-provider/test/fixtures/parity-inspection.ndjson deleted file mode 100644 index 18c16ef484..0000000000 --- a/plugins/antigravity-provider/test/fixtures/parity-inspection.ndjson +++ /dev/null @@ -1,10 +0,0 @@ -{"t_ms": 0, "stream": "inspection", "data": {"type": "configuration", "file": "/owned/.agents/mcp_config.json", "content": {"mcpServers": {"phase2": {"serverUrl": "http://127.0.0.1:/mcp"}}}}} -{"t_ms": 0, "stream": "inspection", "data": {"type": "isolation", "global_mcp_config_exists": {"mcp-second-add-dir": true, "mcp-owned-cwd": true, "mcp-owned-first-add-dir": true, "mcp-agent-file": true, "mcp-agent-definition": true, "mcp-agent-discovered": true, "image-file-external": true}}} -{"t_ms": 0, "stream": "inspection", "data": {"type": "agent_definition", "file": "/owned/agent.md", "content": "---\nname: phase2\ndescription: Session adapter\nmcpServers: {\"phase2\": {\"serverUrl\": \"http://127.0.0.1:/mcp\"}}\n---\nWork in the current workspace.\n"}} -{"t_ms": 0, "stream": "inspection", "data": {"type": "agent_definition", "file": "/owned/.agents/agents/phase2.md", "content": "---\nname: phase2\ndescription: Session adapter\nmainAgent: true\nworkdir: \nmcpServers: {\"phase2\": {\"serverUrl\": \"http://127.0.0.1:/mcp\"}}\n---\n# Identity\nWork in the current workspace.\n"}} -{"t_ms": 0, "stream": "inspection", "data": {"type": "agent_manifest", "file": "/owned/.agents/agents.json", "content": {"entries": [{"path": "/owned/.agents/agents"}]}}} -{"t_ms": 0, "stream": "inspection", "data": {"type": "native_log", "capture": "mcp-agent-file", "line": "W0929 21:29:08.043649 1 session.go:94] Agent \"/owned/agent.md\" not found, falling back to default"}} -{"t_ms": 0, "stream": "inspection", "data": {"type": "native_log", "capture": "mcp-agent-definition", "line": "W0929 21:30:59.337788 1 session.go:94] Agent \"phase2\" not found, falling back to default"}} -{"t_ms": 0, "stream": "inspection", "data": {"type": "native_log", "capture": "mcp-agent-discovered", "line": "W0929 21:33:13.643983 1 session.go:94] Agent \"phase2\" not found, falling back to default"}} -{"t_ms": 0, "stream": "inspection", "data": {"type": "translator_image_part", "function": "view_file", "location": "functionResponse.parts[].inlineData", "mimeType": "image/png", "forwarded": true}} -{"t_ms": 0, "stream": "inspection", "data": {"type": "image", "fixture": "image-file-external.png", "sha256": "abd9ec3ff865aa30218d6184eadff4c0fc1c55d6ada251909d6b4554d64d5336", "expected": "Red triangle on the left, blue circle on the lower right, white background."}} diff --git a/plugins/antigravity-provider/test/fixtures/project-skill.capture.ndjson b/plugins/antigravity-provider/test/fixtures/project-skill.capture.ndjson deleted file mode 100644 index c7e091ebe3..0000000000 --- a/plugins/antigravity-provider/test/fixtures/project-skill.capture.ndjson +++ /dev/null @@ -1,2 +0,0 @@ -{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", "", "--disable-slash-commands"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} -{"t_ms": 5673, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/project-skill.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/project-skill.stderr.ndjson deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/plugins/antigravity-provider/test/fixtures/project-skill.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/project-skill.stdin.ndjson deleted file mode 100644 index 2380ec6f8e..0000000000 --- a/plugins/antigravity-provider/test/fixtures/project-skill.stdin.ndjson +++ /dev/null @@ -1 +0,0 @@ -{"t_ms": 361, "stream": "stdin", "data": {"event": "user", "message": {"content": "Use the phase0-marker skill now."}}} diff --git a/plugins/antigravity-provider/test/fixtures/project-skill.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/project-skill.stdout.ndjson deleted file mode 100644 index d2a8a0b936..0000000000 --- a/plugins/antigravity-provider/test/fixtures/project-skill.stdout.ndjson +++ /dev/null @@ -1,9 +0,0 @@ -{"t_ms": 361, "stream": "stdout", "data": {"event": "init", "conversation_id": "a145f6cf-b974-4ddb-b7eb-58bca63a0092", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "request-review"}}} -{"t_ms": 415, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a145f6cf-b974-4ddb-b7eb-58bca63a0092", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} -{"t_ms": 3806, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a145f6cf-b974-4ddb-b7eb-58bca63a0092", "step_index": 1, "state": "DONE", "step_type": "agent_response", "duration_seconds": 3.389284511, "usage": {"input_tokens": 11270, "output_tokens": 246, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11516}}}} -{"t_ms": 3806, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a145f6cf-b974-4ddb-b7eb-58bca63a0092", "step_index": 2, "state": "ACTIVE", "step_type": "tool", "tool_name": "view_file", "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/.agents/skills/phase0-marker/SKILL.md"}}}}} -{"t_ms": 3806, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a145f6cf-b974-4ddb-b7eb-58bca63a0092", "step_index": 2, "state": "DONE", "step_type": "tool", "tool_name": "view_file", "duration_seconds": 0.003649634, "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/.agents/skills/phase0-marker/SKILL.md"}, "output": "6 lines, 163 bytes"}}}} -{"t_ms": 5190, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a145f6cf-b974-4ddb-b7eb-58bca63a0092", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "PHASE"}}} -{"t_ms": 5216, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a145f6cf-b974-4ddb-b7eb-58bca63a0092", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "0_SKILL_MARKER"}}} -{"t_ms": 5572, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a145f6cf-b974-4ddb-b7eb-58bca63a0092", "step_index": 3, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 1.7500252870000002, "usage": {"input_tokens": 11562, "output_tokens": 9, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11571}}}} -{"t_ms": 5572, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "a145f6cf-b974-4ddb-b7eb-58bca63a0092", "status": "SUCCESS", "response": "PHASE0_SKILL_MARKER\n", "duration_seconds": 5.185824959, "num_turns": 1, "usage": {"input_tokens": 22832, "output_tokens": 255, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 23087}}}} diff --git a/plugins/antigravity-provider/test/fixtures/question-auto-denial.capture.ndjson b/plugins/antigravity-provider/test/fixtures/question-auto-denial.capture.ndjson deleted file mode 100644 index f2e339bb07..0000000000 --- a/plugins/antigravity-provider/test/fixtures/question-auto-denial.capture.ndjson +++ /dev/null @@ -1,2 +0,0 @@ -{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", ""], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} -{"t_ms": 7149, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/question-auto-denial.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/question-auto-denial.stderr.ndjson deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/plugins/antigravity-provider/test/fixtures/question-auto-denial.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/question-auto-denial.stdin.ndjson deleted file mode 100644 index b53fbe5176..0000000000 --- a/plugins/antigravity-provider/test/fixtures/question-auto-denial.stdin.ndjson +++ /dev/null @@ -1 +0,0 @@ -{"t_ms": 298, "stream": "stdin", "data": {"event": "user", "message": {"content": "Use ask_question to ask me which color I prefer, red or blue. Do not choose for me."}}} diff --git a/plugins/antigravity-provider/test/fixtures/question-auto-denial.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/question-auto-denial.stdout.ndjson deleted file mode 100644 index 352ea75867..0000000000 --- a/plugins/antigravity-provider/test/fixtures/question-auto-denial.stdout.ndjson +++ /dev/null @@ -1,14 +0,0 @@ -{"t_ms": 298, "stream": "stdout", "data": {"event": "init", "conversation_id": "e81b4d91-ca0f-4bf9-9841-d44ec1f48997", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "request-review"}}} -{"t_ms": 325, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e81b4d91-ca0f-4bf9-9841-d44ec1f48997", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} -{"t_ms": 4334, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e81b4d91-ca0f-4bf9-9841-d44ec1f48997", "step_index": 1, "state": "DONE", "step_type": "agent_response", "duration_seconds": 3.994201944, "usage": {"input_tokens": 11258, "output_tokens": 297, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11555}}}} -{"t_ms": 4334, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e81b4d91-ca0f-4bf9-9841-d44ec1f48997", "step_index": 2, "state": "DONE", "step_type": "unknown", "duration_seconds": 0.00288314}}} -{"t_ms": 6347, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e81b4d91-ca0f-4bf9-9841-d44ec1f48997", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "You"}}} -{"t_ms": 6676, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e81b4d91-ca0f-4bf9-9841-d44ec1f48997", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " skipped ans"}}} -{"t_ms": 6702, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e81b4d91-ca0f-4bf9-9841-d44ec1f48997", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "wering the question. Let"}}} -{"t_ms": 6728, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e81b4d91-ca0f-4bf9-9841-d44ec1f48997", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " me know if you'd like t"}}} -{"t_ms": 6753, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e81b4d91-ca0f-4bf9-9841-d44ec1f48997", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "o choose a c"}}} -{"t_ms": 6778, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e81b4d91-ca0f-4bf9-9841-d44ec1f48997", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "olor or if there's anyth"}}} -{"t_ms": 6804, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e81b4d91-ca0f-4bf9-9841-d44ec1f48997", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ing else I can help you "}}} -{"t_ms": 6829, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e81b4d91-ca0f-4bf9-9841-d44ec1f48997", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "with!"}}} -{"t_ms": 7057, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e81b4d91-ca0f-4bf9-9841-d44ec1f48997", "step_index": 3, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 2.727510169, "usage": {"input_tokens": 11366, "output_tokens": 31, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11397}}}} -{"t_ms": 7057, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "e81b4d91-ca0f-4bf9-9841-d44ec1f48997", "status": "SUCCESS", "response": "You skipped answering the question. Let me know if you'd like to choose a color or if there's anything else I can help you with!\n", "duration_seconds": 6.748385314, "num_turns": 1, "usage": {"input_tokens": 22624, "output_tokens": 328, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 22952}}}} diff --git a/plugins/antigravity-provider/test/fixtures/resume-after-interrupt.capture.ndjson b/plugins/antigravity-provider/test/fixtures/resume-after-interrupt.capture.ndjson deleted file mode 100644 index 381dc3897e..0000000000 --- a/plugins/antigravity-provider/test/fixtures/resume-after-interrupt.capture.ndjson +++ /dev/null @@ -1,2 +0,0 @@ -{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--conversation", "a2c2132f-0f10-400b-a142-74f82230f449"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} -{"t_ms": 2531, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/resume-after-interrupt.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/resume-after-interrupt.stderr.ndjson deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/plugins/antigravity-provider/test/fixtures/resume-after-interrupt.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/resume-after-interrupt.stdin.ndjson deleted file mode 100644 index 40ea297e7f..0000000000 --- a/plugins/antigravity-provider/test/fixtures/resume-after-interrupt.stdin.ndjson +++ /dev/null @@ -1 +0,0 @@ -{"t_ms": 357, "stream": "stdin", "data": {"event": "user", "message": {"content": "Reply with PHASE0_RECOVERED. No tools."}}} diff --git a/plugins/antigravity-provider/test/fixtures/resume-after-interrupt.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/resume-after-interrupt.stdout.ndjson deleted file mode 100644 index 8b3e527418..0000000000 --- a/plugins/antigravity-provider/test/fixtures/resume-after-interrupt.stdout.ndjson +++ /dev/null @@ -1,7 +0,0 @@ -{"t_ms": 357, "stream": "stdout", "data": {"event": "init", "conversation_id": "a2c2132f-0f10-400b-a142-74f82230f449", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "request-review"}}} -{"t_ms": 386, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a2c2132f-0f10-400b-a142-74f82230f449", "step_index": 2, "state": "DONE", "step_type": "user_input"}}} -{"t_ms": 412, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a2c2132f-0f10-400b-a142-74f82230f449", "step_index": 3, "state": "DONE", "step_type": "system_message", "duration_seconds": 6.4942e-05}}} -{"t_ms": 2228, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a2c2132f-0f10-400b-a142-74f82230f449", "step_index": 4, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "PHASE0_RECOV"}}} -{"t_ms": 2253, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a2c2132f-0f10-400b-a142-74f82230f449", "step_index": 4, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ERED"}}} -{"t_ms": 2380, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a2c2132f-0f10-400b-a142-74f82230f449", "step_index": 4, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 1.9710905699999999, "usage": {"input_tokens": 11524, "output_tokens": 65, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11589}}}} -{"t_ms": 2380, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "a2c2132f-0f10-400b-a142-74f82230f449", "status": "SUCCESS", "response": "PHASE0_RECOVERED\n", "duration_seconds": 216.301687963, "num_turns": 2, "usage": {"input_tokens": 11524, "output_tokens": 65, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11589}}}} diff --git a/plugins/antigravity-provider/test/fixtures/resume.capture.ndjson b/plugins/antigravity-provider/test/fixtures/resume.capture.ndjson deleted file mode 100644 index 4caec009da..0000000000 --- a/plugins/antigravity-provider/test/fixtures/resume.capture.ndjson +++ /dev/null @@ -1,2 +0,0 @@ -{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", "", "--disable-slash-commands", "--conversation", "b966b16a-caf4-448a-bb24-0e34956022b0"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} -{"t_ms": 2317, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/resume.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/resume.stderr.ndjson deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/plugins/antigravity-provider/test/fixtures/resume.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/resume.stdin.ndjson deleted file mode 100644 index e91cc571bd..0000000000 --- a/plugins/antigravity-provider/test/fixtures/resume.stdin.ndjson +++ /dev/null @@ -1 +0,0 @@ -{"t_ms": 288, "stream": "stdin", "data": {"event": "user", "message": {"role": "user", "content": "What secret word did I give you? No tools."}}} diff --git a/plugins/antigravity-provider/test/fixtures/resume.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/resume.stdout.ndjson deleted file mode 100644 index d685e83cfb..0000000000 --- a/plugins/antigravity-provider/test/fixtures/resume.stdout.ndjson +++ /dev/null @@ -1,8 +0,0 @@ -{"t_ms": 288, "stream": "stdout", "data": {"event": "init", "conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "request-review"}}} -{"t_ms": 333, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 4, "state": "DONE", "step_type": "user_input"}}} -{"t_ms": 333, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 5, "state": "DONE", "step_type": "system_message", "duration_seconds": 4.3562e-05}}} -{"t_ms": 1969, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 6, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "The secret"}}} -{"t_ms": 2045, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 6, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " word you gave me is **C"}}} -{"t_ms": 2070, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 6, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "OBALT**."}}} -{"t_ms": 2172, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 6, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 1.821180269, "usage": {"input_tokens": 11815, "output_tokens": 12, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11827}}}} -{"t_ms": 2172, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "status": "SUCCESS", "response": "The secret word you gave me is **COBALT**.\n", "duration_seconds": 8.574573367, "num_turns": 3, "usage": {"input_tokens": 34682, "output_tokens": 481, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 35163}}}} diff --git a/plugins/antigravity-provider/test/fixtures/schema-prompt.capture.ndjson b/plugins/antigravity-provider/test/fixtures/schema-prompt.capture.ndjson deleted file mode 100644 index 9964112608..0000000000 --- a/plugins/antigravity-provider/test/fixtures/schema-prompt.capture.ndjson +++ /dev/null @@ -1,3 +0,0 @@ -{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} -{"t_ms": 30706, "stream": "capture", "data": {"timeout_waiting_for": "result", "seconds": 30}} -{"t_ms": 30794, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/schema-prompt.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/schema-prompt.stderr.ndjson deleted file mode 100644 index 0a830c1e99..0000000000 --- a/plugins/antigravity-provider/test/fixtures/schema-prompt.stderr.ndjson +++ /dev/null @@ -1 +0,0 @@ -{"t_ms": 677, "stream": "stderr", "data": "warning: ignoring unsupported stream input message event \"prompt\""} diff --git a/plugins/antigravity-provider/test/fixtures/schema-prompt.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/schema-prompt.stdin.ndjson deleted file mode 100644 index 2831eab45f..0000000000 --- a/plugins/antigravity-provider/test/fixtures/schema-prompt.stdin.ndjson +++ /dev/null @@ -1 +0,0 @@ -{"t_ms": 677, "stream": "stdin", "data": {"event": "prompt", "prompt": "Reply PHASE0_HELLO."}} diff --git a/plugins/antigravity-provider/test/fixtures/schema-prompt.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/schema-prompt.stdout.ndjson deleted file mode 100644 index 1b83a4d41e..0000000000 --- a/plugins/antigravity-provider/test/fixtures/schema-prompt.stdout.ndjson +++ /dev/null @@ -1 +0,0 @@ -{"t_ms": 676, "stream": "stdout", "data": {"event": "init", "conversation_id": "11a830e0-80b1-4f55-ab34-0670be20a722", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "request-review"}}} diff --git a/plugins/antigravity-provider/test/fixtures/schema-user-message.capture.ndjson b/plugins/antigravity-provider/test/fixtures/schema-user-message.capture.ndjson deleted file mode 100644 index 8db1f98058..0000000000 --- a/plugins/antigravity-provider/test/fixtures/schema-user-message.capture.ndjson +++ /dev/null @@ -1,2 +0,0 @@ -{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} -{"t_ms": 490, "stream": "capture", "data": {"exit_code": 1}} diff --git a/plugins/antigravity-provider/test/fixtures/schema-user-message.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/schema-user-message.stderr.ndjson deleted file mode 100644 index 30d0fa5adb..0000000000 --- a/plugins/antigravity-provider/test/fixtures/schema-user-message.stderr.ndjson +++ /dev/null @@ -1 +0,0 @@ -{"t_ms": 386, "stream": "stderr", "data": "error: failed to decode stream input: json: cannot unmarshal string into Go struct field streamInputMessage.message of type printmode.streamInputUserMessage"} diff --git a/plugins/antigravity-provider/test/fixtures/schema-user-message.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/schema-user-message.stdin.ndjson deleted file mode 100644 index cbedcbaceb..0000000000 --- a/plugins/antigravity-provider/test/fixtures/schema-user-message.stdin.ndjson +++ /dev/null @@ -1 +0,0 @@ -{"t_ms": 383, "stream": "stdin", "data": {"event": "user_message", "message": "Reply PHASE0_HELLO."}} diff --git a/plugins/antigravity-provider/test/fixtures/schema-user-message.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/schema-user-message.stdout.ndjson deleted file mode 100644 index 333d3a9b8d..0000000000 --- a/plugins/antigravity-provider/test/fixtures/schema-user-message.stdout.ndjson +++ /dev/null @@ -1,2 +0,0 @@ -{"t_ms": 383, "stream": "stdout", "data": {"event": "init", "conversation_id": "b70d4858-e4ed-469c-84d6-8fec25ceb1cb", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "request-review"}}} -{"t_ms": 386, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "b70d4858-e4ed-469c-84d6-8fec25ceb1cb", "status": "ERROR", "response": "", "error": "failed to decode stream input: json: cannot unmarshal string into Go struct field streamInputMessage.message of type printmode.streamInputUserMessage", "duration_seconds": 0, "num_turns": 0, "usage": {"input_tokens": 0, "output_tokens": 0, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 0}}}} diff --git a/plugins/antigravity-provider/test/fixtures/schema-user.capture.ndjson b/plugins/antigravity-provider/test/fixtures/schema-user.capture.ndjson deleted file mode 100644 index 205f7c638b..0000000000 --- a/plugins/antigravity-provider/test/fixtures/schema-user.capture.ndjson +++ /dev/null @@ -1,2 +0,0 @@ -{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} -{"t_ms": 426, "stream": "capture", "data": {"exit_code": 1}} diff --git a/plugins/antigravity-provider/test/fixtures/schema-user.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/schema-user.stderr.ndjson deleted file mode 100644 index 7ae241a87e..0000000000 --- a/plugins/antigravity-provider/test/fixtures/schema-user.stderr.ndjson +++ /dev/null @@ -1 +0,0 @@ -{"t_ms": 342, "stream": "stderr", "data": "error: failed to decode stream input: json: cannot unmarshal string into Go struct field streamInputMessage.message of type printmode.streamInputUserMessage"} diff --git a/plugins/antigravity-provider/test/fixtures/schema-user.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/schema-user.stdin.ndjson deleted file mode 100644 index 5523b07666..0000000000 --- a/plugins/antigravity-provider/test/fixtures/schema-user.stdin.ndjson +++ /dev/null @@ -1 +0,0 @@ -{"t_ms": 338, "stream": "stdin", "data": {"event": "user", "message": "Reply PHASE0_HELLO."}} diff --git a/plugins/antigravity-provider/test/fixtures/schema-user.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/schema-user.stdout.ndjson deleted file mode 100644 index 324e16471e..0000000000 --- a/plugins/antigravity-provider/test/fixtures/schema-user.stdout.ndjson +++ /dev/null @@ -1,2 +0,0 @@ -{"t_ms": 338, "stream": "stdout", "data": {"event": "init", "conversation_id": "f57811d9-3f9d-42cc-870a-ffdaa85ba608", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "request-review"}}} -{"t_ms": 342, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "f57811d9-3f9d-42cc-870a-ffdaa85ba608", "status": "ERROR", "response": "", "error": "failed to decode stream input: json: cannot unmarshal string into Go struct field streamInputMessage.message of type printmode.streamInputUserMessage", "duration_seconds": 0, "num_turns": 0, "usage": {"input_tokens": 0, "output_tokens": 0, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 0}}}} diff --git a/plugins/antigravity-provider/test/fixtures/sigint-mid-text.capture.ndjson b/plugins/antigravity-provider/test/fixtures/sigint-mid-text.capture.ndjson deleted file mode 100644 index 744e6a79b1..0000000000 --- a/plugins/antigravity-provider/test/fixtures/sigint-mid-text.capture.ndjson +++ /dev/null @@ -1,3 +0,0 @@ -{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", "", "--disable-slash-commands", "--dangerously-skip-permissions"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} -{"t_ms": 2000, "stream": "capture", "data": {"signal": "SIGINT", "trigger": {"conversation_id": "a2c2132f-0f10-400b-a142-74f82230f449", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "# The Architecture of In"}}} -{"t_ms": 2141, "stream": "capture", "data": {"exit_code": 1}} diff --git a/plugins/antigravity-provider/test/fixtures/sigint-mid-text.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/sigint-mid-text.stderr.ndjson deleted file mode 100644 index 49b703e524..0000000000 --- a/plugins/antigravity-provider/test/fixtures/sigint-mid-text.stderr.ndjson +++ /dev/null @@ -1 +0,0 @@ -{"t_ms": 2025, "stream": "stderr", "data": "error: interrupted"} diff --git a/plugins/antigravity-provider/test/fixtures/sigint-mid-text.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/sigint-mid-text.stdin.ndjson deleted file mode 100644 index 06c4053234..0000000000 --- a/plugins/antigravity-provider/test/fixtures/sigint-mid-text.stdin.ndjson +++ /dev/null @@ -1 +0,0 @@ -{"t_ms": 310, "stream": "stdin", "data": {"event": "user", "message": {"content": "Write a long 2000-word essay on prime numbers. No tools."}}} diff --git a/plugins/antigravity-provider/test/fixtures/sigint-mid-text.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/sigint-mid-text.stdout.ndjson deleted file mode 100644 index 63b9bd96f1..0000000000 --- a/plugins/antigravity-provider/test/fixtures/sigint-mid-text.stdout.ndjson +++ /dev/null @@ -1,4 +0,0 @@ -{"t_ms": 309, "stream": "stdout", "data": {"event": "init", "conversation_id": "a2c2132f-0f10-400b-a142-74f82230f449", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "always-proceed"}}} -{"t_ms": 337, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a2c2132f-0f10-400b-a142-74f82230f449", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} -{"t_ms": 2000, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a2c2132f-0f10-400b-a142-74f82230f449", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "# The Architecture of In"}}} -{"t_ms": 2025, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "a2c2132f-0f10-400b-a142-74f82230f449", "status": "ERROR", "response": "# The Architecture of In", "error": "interrupted", "duration_seconds": 1.690084523, "num_turns": 1, "usage": {"input_tokens": 0, "output_tokens": 0, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 0}}}} diff --git a/plugins/antigravity-provider/test/fixtures/sigint-mid-tool.capture.ndjson b/plugins/antigravity-provider/test/fixtures/sigint-mid-tool.capture.ndjson deleted file mode 100644 index d4774ef9e5..0000000000 --- a/plugins/antigravity-provider/test/fixtures/sigint-mid-tool.capture.ndjson +++ /dev/null @@ -1,3 +0,0 @@ -{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", "", "--dangerously-skip-permissions"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} -{"t_ms": 4486, "stream": "capture", "data": {"signal": "SIGINT", "trigger": {"conversation_id": "b3c8b9d6-c0ca-4444-8fd3-8bc86de8e6ba", "step_index": 2, "state": "ACTIVE", "step_type": "tool", "tool_name": "run_command", "tool_info": {"name": "run_command", "parameters": {"CommandLine": "sleep 30; printf FINISHED"}}}}} -{"t_ms": 4716, "stream": "capture", "data": {"exit_code": 1}} diff --git a/plugins/antigravity-provider/test/fixtures/sigint-mid-tool.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/sigint-mid-tool.stderr.ndjson deleted file mode 100644 index 40ea382edb..0000000000 --- a/plugins/antigravity-provider/test/fixtures/sigint-mid-tool.stderr.ndjson +++ /dev/null @@ -1 +0,0 @@ -{"t_ms": 4489, "stream": "stderr", "data": "error: interrupted"} diff --git a/plugins/antigravity-provider/test/fixtures/sigint-mid-tool.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/sigint-mid-tool.stdin.ndjson deleted file mode 100644 index 7d34666611..0000000000 --- a/plugins/antigravity-provider/test/fixtures/sigint-mid-tool.stdin.ndjson +++ /dev/null @@ -1 +0,0 @@ -{"t_ms": 371, "stream": "stdin", "data": {"event": "user", "message": {"content": "Use run_command to run sleep 30; printf FINISHED in , WaitMsBeforeAsync 1000. Wait until it finishes."}}} diff --git a/plugins/antigravity-provider/test/fixtures/sigint-mid-tool.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/sigint-mid-tool.stdout.ndjson deleted file mode 100644 index ea8cee886f..0000000000 --- a/plugins/antigravity-provider/test/fixtures/sigint-mid-tool.stdout.ndjson +++ /dev/null @@ -1,5 +0,0 @@ -{"t_ms": 371, "stream": "stdout", "data": {"event": "init", "conversation_id": "b3c8b9d6-c0ca-4444-8fd3-8bc86de8e6ba", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "always-proceed"}}} -{"t_ms": 399, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b3c8b9d6-c0ca-4444-8fd3-8bc86de8e6ba", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} -{"t_ms": 4086, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b3c8b9d6-c0ca-4444-8fd3-8bc86de8e6ba", "step_index": 1, "state": "DONE", "step_type": "agent_response", "duration_seconds": 3.660004339, "usage": {"input_tokens": 11272, "output_tokens": 247, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11519}}}} -{"t_ms": 4086, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b3c8b9d6-c0ca-4444-8fd3-8bc86de8e6ba", "step_index": 2, "state": "ACTIVE", "step_type": "tool", "tool_name": "run_command", "tool_info": {"name": "run_command", "parameters": {"CommandLine": "sleep 30; printf FINISHED"}}}}} -{"t_ms": 4489, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "b3c8b9d6-c0ca-4444-8fd3-8bc86de8e6ba", "status": "ERROR", "response": "", "error": "interrupted", "duration_seconds": 4.114884661, "num_turns": 1, "usage": {"input_tokens": 11272, "output_tokens": 247, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11519}}}} diff --git a/plugins/antigravity-provider/test/fixtures/sigterm.capture.ndjson b/plugins/antigravity-provider/test/fixtures/sigterm.capture.ndjson deleted file mode 100644 index eafb4b76ed..0000000000 --- a/plugins/antigravity-provider/test/fixtures/sigterm.capture.ndjson +++ /dev/null @@ -1,3 +0,0 @@ -{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", "", "--disable-slash-commands", "--dangerously-skip-permissions"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} -{"t_ms": 316, "stream": "capture", "data": {"signal": "SIGTERM", "trigger": {"conversation_id": "21c82404-cd16-4781-a827-eb7186d61b7e", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} -{"t_ms": 467, "stream": "capture", "data": {"exit_code": 1}} diff --git a/plugins/antigravity-provider/test/fixtures/sigterm.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/sigterm.stderr.ndjson deleted file mode 100644 index ee58454e54..0000000000 --- a/plugins/antigravity-provider/test/fixtures/sigterm.stderr.ndjson +++ /dev/null @@ -1 +0,0 @@ -{"t_ms": 341, "stream": "stderr", "data": "error: interrupted"} diff --git a/plugins/antigravity-provider/test/fixtures/sigterm.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/sigterm.stdin.ndjson deleted file mode 100644 index 27e2a28a9c..0000000000 --- a/plugins/antigravity-provider/test/fixtures/sigterm.stdin.ndjson +++ /dev/null @@ -1 +0,0 @@ -{"t_ms": 289, "stream": "stdin", "data": {"event": "user", "message": {"content": "Write a long essay on prime numbers. No tools."}}} diff --git a/plugins/antigravity-provider/test/fixtures/sigterm.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/sigterm.stdout.ndjson deleted file mode 100644 index 1fe0f471e1..0000000000 --- a/plugins/antigravity-provider/test/fixtures/sigterm.stdout.ndjson +++ /dev/null @@ -1,3 +0,0 @@ -{"t_ms": 289, "stream": "stdout", "data": {"event": "init", "conversation_id": "21c82404-cd16-4781-a827-eb7186d61b7e", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "always-proceed"}}} -{"t_ms": 316, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "21c82404-cd16-4781-a827-eb7186d61b7e", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} -{"t_ms": 341, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "21c82404-cd16-4781-a827-eb7186d61b7e", "status": "ERROR", "response": "", "error": "interrupted", "duration_seconds": 0, "num_turns": 1, "usage": {"input_tokens": 0, "output_tokens": 0, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 0}}}} diff --git a/plugins/antigravity-provider/test/fixtures/slash-disabled.capture.ndjson b/plugins/antigravity-provider/test/fixtures/slash-disabled.capture.ndjson deleted file mode 100644 index 72dc5418fc..0000000000 --- a/plugins/antigravity-provider/test/fixtures/slash-disabled.capture.ndjson +++ /dev/null @@ -1,2 +0,0 @@ -{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--disable-slash-commands"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} -{"t_ms": 9267, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/slash-disabled.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/slash-disabled.stderr.ndjson deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/plugins/antigravity-provider/test/fixtures/slash-disabled.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/slash-disabled.stdin.ndjson deleted file mode 100644 index fc08c01367..0000000000 --- a/plugins/antigravity-provider/test/fixtures/slash-disabled.stdin.ndjson +++ /dev/null @@ -1 +0,0 @@ -{"t_ms": 292, "stream": "stdin", "data": {"event": "user", "message": {"content": "/help"}}} diff --git a/plugins/antigravity-provider/test/fixtures/slash-disabled.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/slash-disabled.stdout.ndjson deleted file mode 100644 index 5050830bb3..0000000000 --- a/plugins/antigravity-provider/test/fixtures/slash-disabled.stdout.ndjson +++ /dev/null @@ -1,84 +0,0 @@ -{"t_ms": 291, "stream": "stdout", "data": {"event": "init", "conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "request-review"}}} -{"t_ms": 318, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} -{"t_ms": 3215, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 1, "state": "DONE", "step_type": "agent_response", "duration_seconds": 2.874473262, "usage": {"input_tokens": 11225, "output_tokens": 193, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11418}}}} -{"t_ms": 3215, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 2, "state": "ACTIVE", "step_type": "tool", "tool_name": "view_file", "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/.gemini/antigravity-cli/builtin/skills/antigravity_guide/SKILL.md"}}}}} -{"t_ms": 3215, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 2, "state": "DONE", "step_type": "tool", "tool_name": "view_file", "duration_seconds": 0.003097441, "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/.gemini/antigravity-cli/builtin/skills/antigravity_guide/SKILL.md"}, "output": "56 lines, 3023 bytes"}}}} -{"t_ms": 5737, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 3, "state": "DONE", "step_type": "agent_response", "duration_seconds": 2.513209521, "usage": {"input_tokens": 12487, "output_tokens": 73, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12560}}}} -{"t_ms": 5737, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 4, "state": "ACTIVE", "step_type": "tool", "tool_name": "view_file", "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/.gemini/antigravity-cli/builtin/skills/antigravity_guide/references/cli.md"}}}}} -{"t_ms": 5737, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 4, "state": "DONE", "step_type": "tool", "tool_name": "view_file", "duration_seconds": 0.003075841, "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/.gemini/antigravity-cli/builtin/skills/antigravity_guide/references/cli.md"}, "output": "42 lines, 1686 bytes"}}}} -{"t_ms": 6974, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "Here is"}}} -{"t_ms": 7051, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " a quick guide to using "}}} -{"t_ms": 7076, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "**Antigravit"}}} -{"t_ms": 7101, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "y (AGY)** and its availa"}}} -{"t_ms": 7127, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ble slash"}}} -{"t_ms": 7177, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " commands:\n\n### Available Slash Comm"}}} -{"t_ms": 7203, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ands\n\n* **/p"}}} -{"t_ms": 7228, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "lan** – Initiates deta"}}} -{"t_ms": 7254, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "iled step-by"}}} -{"t_ms": 7279, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "-step planning and archi"}}} -{"t_ms": 7305, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "tecture desi"}}} -{"t_ms": 7330, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "gn"}}} -{"t_ms": 7356, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " before making code changes.\n* **/gr"}}} -{"t_ms": 7381, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ill-me** –"}}} -{"t_ms": 7407, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " Starts an interactive i"}}} -{"t_ms": 7432, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "nterview/rev"}}} -{"t_ms": 7458, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "iew session to refine re"}}} -{"t_ms": 7483, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "quirements and"}}} -{"t_ms": 7535, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " architectural decisions"}}} -{"t_ms": 7560, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": ".\n* **/goal*"}}} -{"t_ms": 7586, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "* – Sets a high-level,"}}} -{"t_ms": 7611, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " long-running goal where"}}} -{"t_ms": 7637, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " the agent w"}}} -{"t_ms": 7662, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "orks autonomously and"}}} -{"t_ms": 7763, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " thoroughly "}}} -{"t_ms": 7789, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "until completion.\n* **/s"}}} -{"t_ms": 7814, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "chedule** "}}} -{"t_ms": 7840, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "– Sets up one-shot tim"}}} -{"t_ms": 7865, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ers or recur"}}} -{"t_ms": 7890, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ring cron jobs for periodic checks a"}}} -{"t_ms": 7916, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "nd background tasks.\n* *"}}} -{"t_ms": 7941, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "*/browser** "}}} -{"t_ms": 7966, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "– Directs browser auto"}}} -{"t_ms": 7992, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "mation, test"}}} -{"t_ms": 8017, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ing, and web research wo"}}} -{"t_ms": 8042, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "rkflows.\n* **/learn** "}}} -{"t_ms": 8068, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "– Teaches the agent pr"}}} -{"t_ms": 8094, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "eferences, project rules"}}} -{"t_ms": 8119, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": ", or recurri"}}} -{"t_ms": 8144, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ng setup procedures to p"}}} -{"t_ms": 8169, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ersist across future ses"}}} -{"t_ms": 8195, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "sions.\n\n---\n"}}} -{"t_ms": 8220, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "\n### What I Can Help You"}}} -{"t_ms": 8246, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " With\n\n*"}}} -{"t_ms": 8271, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " **Code Exploration & Ed"}}} -{"t_ms": 8296, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "iting**: Sea"}}} -{"t_ms": 8322, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "rch codebase, view/edit "}}} -{"t_ms": 8347, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "files, run tests, and de"}}} -{"t_ms": 8372, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "bug issues.\n"}}} -{"t_ms": 8398, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "* **Ref"}}} -{"t_ms": 8500, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "actoring & F"}}} -{"t_ms": 8525, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "eature Development**: Pl"}}} -{"t_ms": 8550, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "an, implemen"}}} -{"t_ms": 8576, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "t, and verify multi-file"}}} -{"t_ms": 8601, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " features.\n*"}}} -{"t_ms": 8626, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " **Automation & Subagent"}}} -{"t_ms": 8652, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "s**: Run concurrent task"}}} -{"t_ms": 8677, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "s"}}} -{"t_ms": 8703, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": ", terminal c"}}} -{"t_ms": 8728, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ommands, or "}}} -{"t_ms": 8754, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "delegate complex tasks t"}}} -{"t_ms": 8779, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "o subagents.\n* **Customi"}}} -{"t_ms": 8804, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "zations**: C"}}} -{"t_ms": 8830, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "onfigure [skills](file:/"}}} -{"t_ms": 8855, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "//slash-disab"}}} -{"t_ms": 8881, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "led-home/.gemini/antigra"}}} -{"t_ms": 8906, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "vity-cli/builtin/skills/"}}} -{"t_ms": 8931, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "agy-customiz"}}} -{"t_ms": 8957, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ations/SKILL.md), wor"}}} -{"t_ms": 8982, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "kspace rules"}}} -{"t_ms": 9007, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": ", hooks, and MCP servers"}}} -{"t_ms": 9033, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": ".\n\nLet me kn"}}} -{"t_ms": 9058, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ow what you would like t"}}} -{"t_ms": 9085, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "o work on!"}}} -{"t_ms": 9136, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 3.416356468, "usage": {"input_tokens": 13315, "output_tokens": 309, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 13624}}}} -{"t_ms": 9162, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "status": "SUCCESS", "response": "Here is a quick guide to using **Antigravity (AGY)** and its available slash commands:\n\n### Available Slash Commands\n\n* **/plan** – Initiates detailed step-by-step planning and architecture design before making code changes.\n* **/grill-me** – Starts an interactive interview/review session to refine requirements and architectural decisions.\n* **/goal** – Sets a high-level, long-running goal where the agent works autonomously and thoroughly until completion.\n* **/schedule** – Sets up one-shot timers or recurring cron jobs for periodic checks and background tasks.\n* **/browser** – Directs browser automation, testing, and web research workflows.\n* **/learn** – Teaches the agent preferences, project rules, or recurring setup procedures to persist across future sessions.\n\n---\n\n### What I Can Help You With\n\n* **Code Exploration & Editing**: Search codebase, view/edit files, run tests, and debug issues.\n* **Refactoring & Feature Development**: Plan, implement, and verify multi-file features.\n* **Automation & Subagents**: Run concurrent tasks, terminal commands, or delegate complex tasks to subagents.\n* **Customizations**: Configure [skills](file:///.gemini/antigravity-cli/builtin/skills/agy-customizations/SKILL.md), workspace rules, hooks, and MCP servers.\n\nLet me know what you would like to work on!\n", "duration_seconds": 8.84160525, "num_turns": 1, "usage": {"input_tokens": 37027, "output_tokens": 575, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 37602}}}} diff --git a/plugins/antigravity-provider/test/fixtures/slash-enabled.capture.ndjson b/plugins/antigravity-provider/test/fixtures/slash-enabled.capture.ndjson deleted file mode 100644 index 852d00e9cd..0000000000 --- a/plugins/antigravity-provider/test/fixtures/slash-enabled.capture.ndjson +++ /dev/null @@ -1,2 +0,0 @@ -{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} -{"t_ms": 483, "stream": "capture", "data": {"exit_code": 2}} diff --git a/plugins/antigravity-provider/test/fixtures/slash-enabled.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/slash-enabled.stderr.ndjson deleted file mode 100644 index cf7c668f40..0000000000 --- a/plugins/antigravity-provider/test/fixtures/slash-enabled.stderr.ndjson +++ /dev/null @@ -1 +0,0 @@ -{"t_ms": 375, "stream": "stderr", "data": "error: /help is answered by the CLI itself and is unavailable with --input-format stream-json; run it as its own --print /help invocation"} diff --git a/plugins/antigravity-provider/test/fixtures/slash-enabled.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/slash-enabled.stdin.ndjson deleted file mode 100644 index 73da2a88d2..0000000000 --- a/plugins/antigravity-provider/test/fixtures/slash-enabled.stdin.ndjson +++ /dev/null @@ -1 +0,0 @@ -{"t_ms": 374, "stream": "stdin", "data": {"event": "user", "message": {"content": "/help"}}} diff --git a/plugins/antigravity-provider/test/fixtures/slash-enabled.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/slash-enabled.stdout.ndjson deleted file mode 100644 index 072ae3c437..0000000000 --- a/plugins/antigravity-provider/test/fixtures/slash-enabled.stdout.ndjson +++ /dev/null @@ -1,2 +0,0 @@ -{"t_ms": 374, "stream": "stdout", "data": {"event": "init", "conversation_id": "ca3eefcf-2a10-470a-ac64-81f29d305c40", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "request-review"}}} -{"t_ms": 375, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "ca3eefcf-2a10-470a-ac64-81f29d305c40", "status": "ERROR", "response": "", "error": "/help is answered by the CLI itself and is unavailable with --input-format stream-json; run it as its own --print /help invocation", "duration_seconds": 0, "num_turns": 0, "usage": {"input_tokens": 0, "output_tokens": 0, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 0}}}} diff --git a/plugins/antigravity-provider/test/fixtures/state-inspection.ndjson b/plugins/antigravity-provider/test/fixtures/state-inspection.ndjson deleted file mode 100644 index 2155c5cd9f..0000000000 --- a/plugins/antigravity-provider/test/fixtures/state-inspection.ndjson +++ /dev/null @@ -1,6 +0,0 @@ -{"stream": "inspection", "data": {"command": "python3 /finalize.py", "sha256": "574b0234656c44564f3ff3b013225c86f14ae79b67d0f3b45f6aaf1b1625440e", "version_fixture": "version.stdout.ndjson:1", "operation": "hashlib.sha256( bytes)"}} -{"stream": "inspection", "data": {"command": "HOME= strace -f -e trace=file -o /files.trace /.local/opt/agy/1.2.13/agy --input-format stream-json --output-format stream-json", "accessed_state_paths": ["/.gemini/antigravity-cli/antigravity-oauth-token", "/.gemini/antigravity-cli/conversation_summaries.db", "/.gemini/antigravity-cli/conversations", "/.gemini/antigravity-cli/mcp_config.json", "/.gemini/antigravity-cli/secplugins/config.json", "/.gemini/antigravity-cli/settings.json", "/.gemini/antigravity/conversations", "/.gemini/config/config.json", "/.gemini/config/mcp_config.json"], "outside_isolated_home_gemini_accesses": []}} -{"stream": "inspection", "data": {"command": "python3 /finalize.py", "database": "/.gemini/antigravity-cli/conversation_summaries.db", "query": "SELECT conversation_id,title,preview,step_count,last_modified_time,workspace_uris,project_id,agent_name,parent_conversation_id,app_data_dir FROM conversation_summaries", "rows": [{"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "title": "Secret Word Arithmetic Calculation", "preview": "Secret Word Arithmetic Calculation", "step_count": 13, "last_modified_time": "2026-09-29 18:42:36.36116556+00:00", "workspace_uris": "[\"file://\"]", "project_id": "default-cli-project", "agent_name": "", "parent_conversation_id": "", "app_data_dir": "antigravity-cli"}]}} -{"stream": "inspection", "data": {"database": "/.gemini/antigravity-cli/conversations/b966b16a-caf4-448a-bb24-0e34956022b0.db", "query": "SELECT name FROM sqlite_master WHERE type='table'", "tables": ["trajectory_meta", "steps", "gen_metadata", "executor_metadata", "parent_references", "trajectory_metadata_blob", "battle_mode_infos"], "steps_count": 13, "note": "Step payloads and metadata are binary blobs; no transcript decoder verified."}} -{"stream": "inspection", "data": {"command": "python3 /finalize.py", "workspace_files": {"sandbox.txt": "PHASE0_EDIT\n", "skip-permissions.txt": "PHASE0_EDIT\n", "plan.txt": "PHASE0_EDIT\n", "accept-edits.txt": "PHASE0_EDIT\n", "default.txt": "PHASE0_EDIT\n", "input.txt": "PHASE0_READ_MARKER\n"}}} -{"stream": "inspection", "data": {"command": "firecrawl scrape --format markdown", "retrieved_manifest": "```json\n{\n \"version\": \"1.2.12\",\n \"url\": \"https://storage.googleapis.com/antigravity-public/antigravity-cli/1.2.12-5784551402897408/linux-x64/cli_linux_x64.tar.gz\",\n \"sha512\": \"d5f0fe7433cb7c43ea878c07627a4fdb82d218f3bef5e6436266f5d9fdd2df145523453b9be0c4250391a64a007f5f42f7faff797bc2b2d502e7efb4874e383a\"\n}\n\n```\n", "note": "Fetch returned 1.2.12; supplied binary reports 1.2.13. Release provenance is not established by this manifest response."}} diff --git a/plugins/antigravity-provider/test/fixtures/stdin-close-mid-turn.capture.ndjson b/plugins/antigravity-provider/test/fixtures/stdin-close-mid-turn.capture.ndjson deleted file mode 100644 index be57e05c35..0000000000 --- a/plugins/antigravity-provider/test/fixtures/stdin-close-mid-turn.capture.ndjson +++ /dev/null @@ -1,3 +0,0 @@ -{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", "", "--disable-slash-commands", "--dangerously-skip-permissions"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} -{"t_ms": 319, "stream": "capture", "data": {"stdin_close": true}} -{"t_ms": 2576, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/stdin-close-mid-turn.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/stdin-close-mid-turn.stderr.ndjson deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/plugins/antigravity-provider/test/fixtures/stdin-close-mid-turn.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/stdin-close-mid-turn.stdin.ndjson deleted file mode 100644 index de27d4d5db..0000000000 --- a/plugins/antigravity-provider/test/fixtures/stdin-close-mid-turn.stdin.ndjson +++ /dev/null @@ -1 +0,0 @@ -{"t_ms": 292, "stream": "stdin", "data": {"event": "user", "message": {"content": "Reply with PHASE0_EOF. No tools."}}} diff --git a/plugins/antigravity-provider/test/fixtures/stdin-close-mid-turn.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/stdin-close-mid-turn.stdout.ndjson deleted file mode 100644 index 88c41c8b1d..0000000000 --- a/plugins/antigravity-provider/test/fixtures/stdin-close-mid-turn.stdout.ndjson +++ /dev/null @@ -1,5 +0,0 @@ -{"t_ms": 292, "stream": "stdout", "data": {"event": "init", "conversation_id": "4a95b7fd-ae73-4b99-9c00-25d7c37f0250", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "always-proceed"}}} -{"t_ms": 319, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "4a95b7fd-ae73-4b99-9c00-25d7c37f0250", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} -{"t_ms": 2230, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "4a95b7fd-ae73-4b99-9c00-25d7c37f0250", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "PHASE0_EOF"}}} -{"t_ms": 2459, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "4a95b7fd-ae73-4b99-9c00-25d7c37f0250", "step_index": 1, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 2.123780285, "usage": {"input_tokens": 11253, "output_tokens": 80, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11333}}}} -{"t_ms": 2459, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "4a95b7fd-ae73-4b99-9c00-25d7c37f0250", "status": "SUCCESS", "response": "PHASE0_EOF\n", "duration_seconds": 2.146186078, "num_turns": 1, "usage": {"input_tokens": 11253, "output_tokens": 80, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11333}}}} diff --git a/plugins/antigravity-provider/test/fixtures/stdin-mid-turn.capture.ndjson b/plugins/antigravity-provider/test/fixtures/stdin-mid-turn.capture.ndjson deleted file mode 100644 index 12fb5138b4..0000000000 --- a/plugins/antigravity-provider/test/fixtures/stdin-mid-turn.capture.ndjson +++ /dev/null @@ -1,2 +0,0 @@ -{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", "", "--disable-slash-commands", "--dangerously-skip-permissions"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} -{"t_ms": 9564, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/stdin-mid-turn.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/stdin-mid-turn.stderr.ndjson deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/plugins/antigravity-provider/test/fixtures/stdin-mid-turn.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/stdin-mid-turn.stdin.ndjson deleted file mode 100644 index 326c02c7d0..0000000000 --- a/plugins/antigravity-provider/test/fixtures/stdin-mid-turn.stdin.ndjson +++ /dev/null @@ -1,2 +0,0 @@ -{"t_ms": 971, "stream": "stdin", "data": {"event": "user", "message": {"content": "Write a 500-word essay about primes. No tools."}}} -{"t_ms": 997, "stream": "stdin", "data": {"event": "user", "message": {"content": "Reply with exactly PHASE0_QUEUED. No tools."}}} diff --git a/plugins/antigravity-provider/test/fixtures/stdin-mid-turn.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/stdin-mid-turn.stdout.ndjson deleted file mode 100644 index bf80956e6a..0000000000 --- a/plugins/antigravity-provider/test/fixtures/stdin-mid-turn.stdout.ndjson +++ /dev/null @@ -1,179 +0,0 @@ -{"t_ms": 971, "stream": "stdout", "data": {"event": "init", "conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "always-proceed"}}} -{"t_ms": 997, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} -{"t_ms": 3035, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "### The Indivisible Atom"}}} -{"t_ms": 3061, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "s of Arithme"}}} -{"t_ms": 3087, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "tic\n\nIn the vast landsca"}}} -{"t_ms": 3112, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "pe of mathem"}}} -{"t_ms": 3137, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "atics, prime numbers sta"}}} -{"t_ms": 3162, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "nd as"}}} -{"t_ms": 3213, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " fundamental building bl"}}} -{"t_ms": 3238, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ocks—the i"}}} -{"t_ms": 3263, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ndivisible atoms of arit"}}} -{"t_ms": 3289, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "hmetic. Defi"}}} -{"t_ms": 3314, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ned simply as integers g"}}} -{"t_ms": 3339, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "reater than "}}} -{"t_ms": 3365, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "one that are divisible o"}}} -{"t_ms": 3390, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "nly by one"}}} -{"t_ms": 3441, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " and themselves, primes possess an e"}}} -{"t_ms": 3466, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "lemental pur"}}} -{"t_ms": 3491, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ity. Yet beneath this de"}}} -{"t_ms": 3517, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ceptive simplicity lies "}}} -{"t_ms": 3542, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "one of the d"}}} -{"t_ms": 3567, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "eepest, most beautiful m"}}} -{"t_ms": 3592, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ysteries in "}}} -{"t_ms": 3618, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "human"}}} -{"t_ms": 3668, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " inquiry: an"}}} -{"t_ms": 3694, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " unending dance between "}}} -{"t_ms": 3719, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "order and ch"}}} -{"t_ms": 3744, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "aos, predictability and "}}} -{"t_ms": 3770, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "randomness.\n"}}} -{"t_ms": 3795, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "\nThe fascination with pr"}}} -{"t_ms": 3820, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "imes stretches back to antiquity. In"}}} -{"t_ms": 3846, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " approximate"}}} -{"t_ms": 3871, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ly 300 BCE, the ancient "}}} -{"t_ms": 3897, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "Greek mathem"}}} -{"t_ms": 3922, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "atician Euclid formulate"}}} -{"t_ms": 3947, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "d one of history’s mos"}}} -{"t_ms": 3972, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "t elegant proofs in his "}}} -{"t_ms": 3998, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "*Elements*, demonstratin"}}} -{"t_ms": 4023, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "g that the s"}}} -{"t_ms": 4048, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "upply of prime numbers i"}}} -{"t_ms": 4074, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "s infinite. "}}} -{"t_ms": 4099, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "If one assumes a finite "}}} -{"t_ms": 4124, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "list of primes, multiply"}}} -{"t_ms": 4150, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ing them tog"}}} -{"t_ms": 4175, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ether and adding one yields a new nu"}}} -{"t_ms": 4201, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "mber that is"}}} -{"t_ms": 4226, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " either prime itself or "}}} -{"t_ms": 4251, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "divisible by"}}} -{"t_ms": 4277, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " a prime not on the orig"}}} -{"t_ms": 4303, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "inal list. Euclid’s pr"}}} -{"t_ms": 4328, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "oof establis"}}} -{"t_ms": 4354, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "hed an eternal truth: no matter how "}}} -{"t_ms": 4379, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "far we trave"}}} -{"t_ms": 4404, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "l along the number line,"}}} -{"t_ms": 4430, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " new primes will continu"}}} -{"t_ms": 4455, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "e to emerge."}}} -{"t_ms": 4481, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "\n\nDespite their infinite"}}} -{"t_ms": 4583, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " nature, primes become i"}}} -{"t_ms": 4608, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ncreasingly sparse as nu"}}} -{"t_ms": 4633, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "mbers grow l"}}} -{"t_ms": 4659, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "arger. For centuries, th"}}} -{"t_ms": 4684, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "eir distribu"}}} -{"t_ms": 4709, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "tion appeared entirely e"}}} -{"t_ms": 4734, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "rratic, defying simple formulas. It "}}} -{"t_ms": 4760, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "was not unti"}}} -{"t_ms": 4785, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "l the nineteenth century"}}} -{"t_ms": 4810, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " that Carl F"}}} -{"t_ms": 4836, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "riedrich Gauss and Adrie"}}} -{"t_ms": 4861, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "n-Marie Legendre conject"}}} -{"t_ms": 4886, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ured the Pri"}}} -{"t_ms": 4912, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "me Number Theorem, later"}}} -{"t_ms": 4937, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " proven in 1896 by Jacqu"}}} -{"t_ms": 4963, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "es Hadamard and Charles "}}} -{"t_ms": 4988, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "Jean de la V"}}} -{"t_ms": 5013, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "allée Poussin. The theo"}}} -{"t_ms": 5038, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "rem revealed a breathtak"}}} -{"t_ms": 5064, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ing macrosco"}}} -{"t_ms": 5089, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "pic regularity: the probabi"}}} -{"t_ms": 5114, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "lity that a "}}} -{"t_ms": 5140, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "randomly chosen integer "}}} -{"t_ms": 5165, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "$N$ is prime is inversel"}}} -{"t_ms": 5190, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "y proportion"}}} -{"t_ms": 5216, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "al to its natural logari"}}} -{"t_ms": 5241, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "thm, $\\ln"}}} -{"t_ms": 5292, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "(N)$. While individual p"}}} -{"t_ms": 5317, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "rimes appear"}}} -{"t_ms": 5343, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " almost randomly, their "}}} -{"t_ms": 5368, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "collective b"}}} -{"t_ms": 5393, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ehavior follows a remark"}}} -{"t_ms": 5418, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ably smooth "}}} -{"t_ms": 5444, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "logarithmic curve.\n\nThis"}}} -{"t_ms": 5469, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " duality between"}}} -{"t_ms": 5495, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " local chaos and global "}}} -{"t_ms": 5520, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "harmony is epitomized by"}}} -{"t_ms": 5546, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " the Riemann"}}} -{"t_ms": 5571, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " Hypothesis, proposed by"}}} -{"t_ms": 5596, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " Bernhard Riemann in 185"}}} -{"t_ms": 5622, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "9. By"}}} -{"t_ms": 5673, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " extending t"}}} -{"t_ms": 5698, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "he zeta function into th"}}} -{"t_ms": 5723, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "e complex pl"}}} -{"t_ms": 5749, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ane, Riemann connected t"}}} -{"t_ms": 5774, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "he distribution of prime"}}} -{"t_ms": 5799, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "s directly t"}}} -{"t_ms": 5825, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "o the non-trivial zeros "}}} -{"t_ms": 5850, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "of this func"}}} -{"t_ms": 5875, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "tion. Widely regarded as the most"}}} -{"t_ms": 5900, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " famous unsolved problem"}}} -{"t_ms": 5926, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " in mathemat"}}} -{"t_ms": 5952, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ics, the Riemann Hypothe"}}} -{"t_ms": 5977, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "sis asserts "}}} -{"t_ms": 6002, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "that all these non-trivi"}}} -{"t_ms": 6028, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "al zeros lie"}}} -{"t_ms": 6053, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " on a"}}} -{"t_ms": 6104, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " single critical line. I"}}} -{"t_ms": 6129, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "f true, it would unlock "}}} -{"t_ms": 6154, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "a profound h"}}} -{"t_ms": 6180, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "armonic symphony governi"}}} -{"t_ms": 6205, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ng the exact fluctuation"}}} -{"t_ms": 6230, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "s of prime n"}}} -{"t_ms": 6256, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "umbers.\n\nBeyond"}}} -{"t_ms": 6332, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " their aesthetic allure in pure math"}}} -{"t_ms": 6357, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ematics, pri"}}} -{"t_ms": 6383, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "me numbers have quietly "}}} -{"t_ms": 6408, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "become the b"}}} -{"t_ms": 6434, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "edrock of modern technological civil"}}} -{"t_ms": 6459, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ization. In "}}} -{"t_ms": 6485, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "the 1970s, the developme"}}} -{"t_ms": 6510, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "nt of asymmetric public-"}}} -{"t_ms": 6535, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "key cryptogr"}}} -{"t_ms": 6561, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "aphy—most notably the "}}} -{"t_ms": 6586, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "RSA algorithm—transfor"}}} -{"t_ms": 6611, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "med prime numbers into i"}}} -{"t_ms": 6637, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ndispensable digital gua"}}} -{"t_ms": 6662, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "rdians. The "}}} -{"t_ms": 6687, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "security of global comme"}}} -{"t_ms": 6713, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "rce, secure "}}} -{"t_ms": 6738, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "communication, and onlin"}}} -{"t_ms": 6763, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "e privacy re"}}} -{"t_ms": 6789, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "sts upon a computational asymmetry: "}}} -{"t_ms": 6814, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "multiplying two large pr"}}} -{"t_ms": 6839, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ime numbers "}}} -{"t_ms": 6865, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "together is nearly insta"}}} -{"t_ms": 6890, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ntaneous, bu"}}} -{"t_ms": 6915, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "t factoring their astron"}}} -{"t_ms": 6941, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "omical product back into"}}} -{"t_ms": 6966, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " its constit"}}} -{"t_ms": 6991, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "uent primes is virtually"}}} -{"t_ms": 7017, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " impossible "}}} -{"t_ms": 7042, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "with classical computing resources.\n"}}} -{"t_ms": 7067, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "\nPrime numbe"}}} -{"t_ms": 7093, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "rs represent a sublime i"}}} -{"t_ms": 7118, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ntersection of philosoph"}}} -{"t_ms": 7143, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "y, abstract "}}} -{"t_ms": 7169, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "theory, and practical ut"}}} -{"t_ms": 7194, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ility. They "}}} -{"t_ms": 7219, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "remind us that the most "}}} -{"t_ms": 7245, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "basic structures in natu"}}} -{"t_ms": 7270, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "re often harbor the deep"}}} -{"t_ms": 7295, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "est questions. As mathem"}}} -{"t_ms": 7320, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "aticians con"}}} -{"t_ms": 7346, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "tinue to explore unresol"}}} -{"t_ms": 7371, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ved enigmas "}}} -{"t_ms": 7397, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "like the Twin Prime Conjecture or th"}}} -{"t_ms": 7422, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "e Goldbach Conjecture, p"}}} -{"t_ms": 7448, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "rimes remain"}}} -{"t_ms": 7473, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " an enduring testament t"}}} -{"t_ms": 7498, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "o the infini"}}} -{"t_ms": 7524, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "te richness waiting to b"}}} -{"t_ms": 7549, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "e uncovered within the s"}}} -{"t_ms": 7574, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "implest conc"}}} -{"t_ms": 7600, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "epts."}}} -{"t_ms": 7650, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 6.647623906, "usage": {"input_tokens": 11252, "output_tokens": 639, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11891}}}} -{"t_ms": 7650, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "status": "SUCCESS", "response": "### The Indivisible Atoms of Arithmetic\n\nIn the vast landscape of mathematics, prime numbers stand as fundamental building blocks—the indivisible atoms of arithmetic. Defined simply as integers greater than one that are divisible only by one and themselves, primes possess an elemental purity. Yet beneath this deceptive simplicity lies one of the deepest, most beautiful mysteries in human inquiry: an unending dance between order and chaos, predictability and randomness.\n\nThe fascination with primes stretches back to antiquity. In approximately 300 BCE, the ancient Greek mathematician Euclid formulated one of history’s most elegant proofs in his *Elements*, demonstrating that the supply of prime numbers is infinite. If one assumes a finite list of primes, multiplying them together and adding one yields a new number that is either prime itself or divisible by a prime not on the original list. Euclid’s proof established an eternal truth: no matter how far we travel along the number line, new primes will continue to emerge.\n\nDespite their infinite nature, primes become increasingly sparse as numbers grow larger. For centuries, their distribution appeared entirely erratic, defying simple formulas. It was not until the nineteenth century that Carl Friedrich Gauss and Adrien-Marie Legendre conjectured the Prime Number Theorem, later proven in 1896 by Jacques Hadamard and Charles Jean de la Vallée Poussin. The theorem revealed a breathtaking macroscopic regularity: the probability that a randomly chosen integer $N$ is prime is inversely proportional to its natural logarithm, $\\ln(N)$. While individual primes appear almost randomly, their collective behavior follows a remarkably smooth logarithmic curve.\n\nThis duality between local chaos and global harmony is epitomized by the Riemann Hypothesis, proposed by Bernhard Riemann in 1859. By extending the zeta function into the complex plane, Riemann connected the distribution of primes directly to the non-trivial zeros of this function. Widely regarded as the most famous unsolved problem in mathematics, the Riemann Hypothesis asserts that all these non-trivial zeros lie on a single critical line. If true, it would unlock a profound harmonic symphony governing the exact fluctuations of prime numbers.\n\nBeyond their aesthetic allure in pure mathematics, prime numbers have quietly become the bedrock of modern technological civilization. In the 1970s, the development of asymmetric public-key cryptography—most notably the RSA algorithm—transformed prime numbers into indispensable digital guardians. The security of global commerce, secure communication, and online privacy rests upon a computational asymmetry: multiplying two large prime numbers together is nearly instantaneous, but factoring their astronomical product back into its constituent primes is virtually impossible with classical computing resources.\n\nPrime numbers represent a sublime intersection of philosophy, abstract theory, and practical utility. They remind us that the most basic structures in nature often harbor the deepest questions. As mathematicians continue to explore unresolved enigmas like the Twin Prime Conjecture or the Goldbach Conjecture, primes remain an enduring testament to the infinite richness waiting to be uncovered within the simplest concepts.\n", "duration_seconds": 6.674385393, "num_turns": 1, "usage": {"input_tokens": 11252, "output_tokens": 639, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11891}}}} -{"t_ms": 7677, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 2, "state": "DONE", "step_type": "user_input"}}} -{"t_ms": 9187, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "PHASE"}}} -{"t_ms": 9213, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "0_QUEUED"}}} -{"t_ms": 9466, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 3, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 1.793731176, "usage": {"input_tokens": 11913, "output_tokens": 6, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11919}}}} -{"t_ms": 9466, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "status": "SUCCESS", "response": "PHASE0_QUEUED\n", "duration_seconds": 8.483804893, "num_turns": 2, "usage": {"input_tokens": 23165, "output_tokens": 645, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 23810}}}} diff --git a/plugins/antigravity-provider/test/fixtures/subagent.capture.ndjson b/plugins/antigravity-provider/test/fixtures/subagent.capture.ndjson deleted file mode 100644 index 854c3c3eba..0000000000 --- a/plugins/antigravity-provider/test/fixtures/subagent.capture.ndjson +++ /dev/null @@ -1,2 +0,0 @@ -{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", "", "--dangerously-skip-permissions"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} -{"t_ms": 13163, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/subagent.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/subagent.stderr.ndjson deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/plugins/antigravity-provider/test/fixtures/subagent.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/subagent.stdin.ndjson deleted file mode 100644 index 0814f42805..0000000000 --- a/plugins/antigravity-provider/test/fixtures/subagent.stdin.ndjson +++ /dev/null @@ -1 +0,0 @@ -{"t_ms": 291, "stream": "stdin", "data": {"event": "user", "message": {"content": "Use define_subagent to define a small subagent named phase0-counter that answers arithmetic only, then invoke_subagent to ask it 2+2. Report its answer. Do not use shell."}}} diff --git a/plugins/antigravity-provider/test/fixtures/subagent.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/subagent.stdout.ndjson deleted file mode 100644 index cec633cda7..0000000000 --- a/plugins/antigravity-provider/test/fixtures/subagent.stdout.ndjson +++ /dev/null @@ -1,23 +0,0 @@ -{"t_ms": 291, "stream": "stdout", "data": {"event": "init", "conversation_id": "eceffa7f-bbef-47d0-a3cf-3daeaf20a829", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "always-proceed"}}} -{"t_ms": 319, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "eceffa7f-bbef-47d0-a3cf-3daeaf20a829", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} -{"t_ms": 3924, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "eceffa7f-bbef-47d0-a3cf-3daeaf20a829", "step_index": 1, "state": "DONE", "step_type": "agent_response", "duration_seconds": 3.580501478, "usage": {"input_tokens": 11252, "output_tokens": 188, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11440}}}} -{"t_ms": 3924, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "eceffa7f-bbef-47d0-a3cf-3daeaf20a829", "step_index": 2, "state": "ACTIVE", "step_type": "tool", "tool_name": "define_subagent", "tool_info": {"name": "define_subagent", "parameters": {"description": "A subagent specialized in answering arithmetic questions only.", "name": "phase0-counter"}}}}} -{"t_ms": 3924, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "eceffa7f-bbef-47d0-a3cf-3daeaf20a829", "step_index": 2, "state": "DONE", "step_type": "tool", "tool_name": "define_subagent", "duration_seconds": 0.003321972, "tool_info": {"name": "define_subagent", "parameters": {"description": "A subagent specialized in answering arithmetic questions only.", "name": "phase0-counter"}, "output": "Subagent \"phase0-counter\" defined successfully. It can now be invoked via invoke_subagent."}}}} -{"t_ms": 6541, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "eceffa7f-bbef-47d0-a3cf-3daeaf20a829", "step_index": 3, "state": "DONE", "step_type": "agent_response", "duration_seconds": 2.634297625, "usage": {"input_tokens": 11433, "output_tokens": 79, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11512}}}} -{"t_ms": 6541, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "eceffa7f-bbef-47d0-a3cf-3daeaf20a829", "step_index": 4, "state": "ACTIVE", "step_type": "subagent", "tool_name": "invoke_subagent", "subagent_info": {"subagents": [{"type_name": "phase0-counter", "role": "Arithmetic Assistant", "initial_prompt": "What is 2+2?"}]}}}} -{"t_ms": 6565, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "eceffa7f-bbef-47d0-a3cf-3daeaf20a829", "step_index": 4, "state": "DONE", "step_type": "subagent", "tool_name": "invoke_subagent", "duration_seconds": 0.01034356, "subagent_info": {"subagents": [{"type_name": "phase0-counter", "role": "Arithmetic Assistant", "initial_prompt": "What is 2+2?", "conversation_id": "b6f02329-7415-49a7-a9a9-1cd09e898d26", "log_uri": "file:///.gemini/antigravity-cli/brain/b6f02329-7415-49a7-a9a9-1cd09e898d26/.system_generated/logs/transcript.jsonl", "workspace_uris": ["file://"]}]}}}} -{"t_ms": 8527, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "eceffa7f-bbef-47d0-a3cf-3daeaf20a829", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "I"}}} -{"t_ms": 8679, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "eceffa7f-bbef-47d0-a3cf-3daeaf20a829", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " have define"}}} -{"t_ms": 8704, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "eceffa7f-bbef-47d0-a3cf-3daeaf20a829", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "d the subagent `phase0-c"}}} -{"t_ms": 8730, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "eceffa7f-bbef-47d0-a3cf-3daeaf20a829", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ounter` and "}}} -{"t_ms": 8755, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "eceffa7f-bbef-47d0-a3cf-3daeaf20a829", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "invoked it with the ques"}}} -{"t_ms": 8780, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "eceffa7f-bbef-47d0-a3cf-3daeaf20a829", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "tion \"What"}}} -{"t_ms": 8806, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "eceffa7f-bbef-47d0-a3cf-3daeaf20a829", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " is 2+2?\". Awaiting its "}}} -{"t_ms": 8831, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "eceffa7f-bbef-47d0-a3cf-3daeaf20a829", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "response."}}} -{"t_ms": 9135, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "eceffa7f-bbef-47d0-a3cf-3daeaf20a829", "step_index": 5, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 2.58161986, "usage": {"input_tokens": 11739, "output_tokens": 31, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11770}}}} -{"t_ms": 9715, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "eceffa7f-bbef-47d0-a3cf-3daeaf20a829", "step_index": 6, "state": "DONE", "step_type": "system_message", "duration_seconds": 3.6749e-05}}} -{"t_ms": 12484, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "eceffa7f-bbef-47d0-a3cf-3daeaf20a829", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "The `phase0-"}}} -{"t_ms": 12510, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "eceffa7f-bbef-47d0-a3cf-3daeaf20a829", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "counter` sub"}}} -{"t_ms": 12610, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "eceffa7f-bbef-47d0-a3cf-3daeaf20a829", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "agent answered: **4**."}}} -{"t_ms": 13042, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "eceffa7f-bbef-47d0-a3cf-3daeaf20a829", "step_index": 7, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 3.329676969, "usage": {"input_tokens": 11886, "output_tokens": 74, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11960}}}} -{"t_ms": 13068, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "eceffa7f-bbef-47d0-a3cf-3daeaf20a829", "status": "SUCCESS", "response": "I have defined the subagent `phase0-counter` and invoked it with the question \"What is 2+2?\". Awaiting its response.\nThe `phase0-counter` subagent answered: **4**.\n", "duration_seconds": 12.746450429, "num_turns": 1, "usage": {"input_tokens": 46310, "output_tokens": 372, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 46682}}}} diff --git a/plugins/antigravity-provider/test/fixtures/text-block.capture.ndjson b/plugins/antigravity-provider/test/fixtures/text-block.capture.ndjson deleted file mode 100644 index b1f7ab6455..0000000000 --- a/plugins/antigravity-provider/test/fixtures/text-block.capture.ndjson +++ /dev/null @@ -1,2 +0,0 @@ -{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", ""], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} -{"t_ms": 4102, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/text-block.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/text-block.stderr.ndjson deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/plugins/antigravity-provider/test/fixtures/text-block.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/text-block.stdin.ndjson deleted file mode 100644 index faeb98b247..0000000000 --- a/plugins/antigravity-provider/test/fixtures/text-block.stdin.ndjson +++ /dev/null @@ -1 +0,0 @@ -{"t_ms": 2020, "stream": "stdin", "data": {"event": "user", "message": {"content": [{"type": "text", "text": "Reply with PHASE0_BLOCK. No tools."}]}}} diff --git a/plugins/antigravity-provider/test/fixtures/text-block.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/text-block.stdout.ndjson deleted file mode 100644 index ee7a7ec32a..0000000000 --- a/plugins/antigravity-provider/test/fixtures/text-block.stdout.ndjson +++ /dev/null @@ -1,5 +0,0 @@ -{"t_ms": 2020, "stream": "stdout", "data": {"event": "init", "conversation_id": "0757bec3-d263-4f90-bf34-7510d638d8c5", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "request-review"}}} -{"t_ms": 2051, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0757bec3-d263-4f90-bf34-7510d638d8c5", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} -{"t_ms": 3939, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0757bec3-d263-4f90-bf34-7510d638d8c5", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "PHASE0_BLOCK"}}} -{"t_ms": 3990, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0757bec3-d263-4f90-bf34-7510d638d8c5", "step_index": 1, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 1.932265932, "usage": {"input_tokens": 11235, "output_tokens": 62, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11297}}}} -{"t_ms": 3990, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "0757bec3-d263-4f90-bf34-7510d638d8c5", "status": "SUCCESS", "response": "PHASE0_BLOCK\n", "duration_seconds": 1.959296182, "num_turns": 1, "usage": {"input_tokens": 11235, "output_tokens": 62, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11297}}}} diff --git a/plugins/antigravity-provider/test/fixtures/text-turn.capture.ndjson b/plugins/antigravity-provider/test/fixtures/text-turn.capture.ndjson deleted file mode 100644 index 1b006f8ab9..0000000000 --- a/plugins/antigravity-provider/test/fixtures/text-turn.capture.ndjson +++ /dev/null @@ -1,2 +0,0 @@ -{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", ""], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} -{"t_ms": 2911, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/text-turn.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/text-turn.stderr.ndjson deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/plugins/antigravity-provider/test/fixtures/text-turn.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/text-turn.stdin.ndjson deleted file mode 100644 index 389b97dece..0000000000 --- a/plugins/antigravity-provider/test/fixtures/text-turn.stdin.ndjson +++ /dev/null @@ -1 +0,0 @@ -{"t_ms": 294, "stream": "stdin", "data": {"event": "user", "message": {"role": "user", "content": "Reply with exactly PHASE0_HELLO. No tools."}}} diff --git a/plugins/antigravity-provider/test/fixtures/text-turn.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/text-turn.stdout.ndjson deleted file mode 100644 index 89fc8a7677..0000000000 --- a/plugins/antigravity-provider/test/fixtures/text-turn.stdout.ndjson +++ /dev/null @@ -1,5 +0,0 @@ -{"t_ms": 294, "stream": "stdout", "data": {"event": "init", "conversation_id": "e5cf1e2d-c715-4328-8e67-4a85c8dc3cda", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "request-review"}}} -{"t_ms": 351, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e5cf1e2d-c715-4328-8e67-4a85c8dc3cda", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} -{"t_ms": 2544, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e5cf1e2d-c715-4328-8e67-4a85c8dc3cda", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "PHASE0_HELLO"}}} -{"t_ms": 2746, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e5cf1e2d-c715-4328-8e67-4a85c8dc3cda", "step_index": 1, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 2.413995705, "usage": {"input_tokens": 11235, "output_tokens": 78, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11313}}}} -{"t_ms": 2772, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "e5cf1e2d-c715-4328-8e67-4a85c8dc3cda", "status": "SUCCESS", "response": "PHASE0_HELLO\n", "duration_seconds": 2.445370438, "num_turns": 1, "usage": {"input_tokens": 11235, "output_tokens": 78, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11313}}}} diff --git a/plugins/antigravity-provider/test/fixtures/tools-accept-edits.capture.ndjson b/plugins/antigravity-provider/test/fixtures/tools-accept-edits.capture.ndjson deleted file mode 100644 index d7c50c17a4..0000000000 --- a/plugins/antigravity-provider/test/fixtures/tools-accept-edits.capture.ndjson +++ /dev/null @@ -1,2 +0,0 @@ -{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", "", "--disable-slash-commands", "--mode", "accept-edits"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} -{"t_ms": 16127, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/tools-accept-edits.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/tools-accept-edits.stderr.ndjson deleted file mode 100644 index 30cf90e622..0000000000 --- a/plugins/antigravity-provider/test/fixtures/tools-accept-edits.stderr.ndjson +++ /dev/null @@ -1 +0,0 @@ -{"t_ms": 15954, "stream": "stderr", "data": "jetski: no output produced — a tool required the \"command\" permission that headless mode cannot prompt for, so it was auto-denied. Add an allow-rule under permissions.allow in settings.json (e.g. command()). Alternatively, re-run with --dangerously-skip-permissions to auto-approve all tools."} diff --git a/plugins/antigravity-provider/test/fixtures/tools-accept-edits.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/tools-accept-edits.stdin.ndjson deleted file mode 100644 index 4d89737919..0000000000 --- a/plugins/antigravity-provider/test/fixtures/tools-accept-edits.stdin.ndjson +++ /dev/null @@ -1,3 +0,0 @@ -{"t_ms": 288, "stream": "stdin", "data": {"event": "user", "message": {"role": "user", "content": "Use view_file to read /input.txt. Report its contents. Do not use a shell."}}} -{"t_ms": 5150, "stream": "stdin", "data": {"event": "user", "message": {"role": "user", "content": "Use write_to_file to create /accept-edits.txt with PHASE0_WRITE. Then use replace_file_content to change PHASE0_WRITE to PHASE0_EDIT. No shell. Do not ask me questions."}}} -{"t_ms": 13611, "stream": "stdin", "data": {"event": "user", "message": {"role": "user", "content": "Use run_command exactly once to run printf PHASE0_SHELL in . Report the output. Do not ask me questions."}}} diff --git a/plugins/antigravity-provider/test/fixtures/tools-accept-edits.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/tools-accept-edits.stdout.ndjson deleted file mode 100644 index 57d50ad776..0000000000 --- a/plugins/antigravity-provider/test/fixtures/tools-accept-edits.stdout.ndjson +++ /dev/null @@ -1,40 +0,0 @@ -{"t_ms": 288, "stream": "stdout", "data": {"event": "init", "conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "request-review"}}} -{"t_ms": 315, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} -{"t_ms": 2807, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 1, "state": "DONE", "step_type": "agent_response", "duration_seconds": 2.468344855, "usage": {"input_tokens": 11272, "output_tokens": 149, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11421}}}} -{"t_ms": 2807, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 2, "state": "ACTIVE", "step_type": "tool", "tool_name": "view_file", "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/input.txt"}}}}} -{"t_ms": 2807, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 2, "state": "DONE", "step_type": "tool", "tool_name": "view_file", "duration_seconds": 0.002720597, "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/input.txt"}, "output": "2 lines, 19 bytes"}}}} -{"t_ms": 4820, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "The contents"}}} -{"t_ms": 4870, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " of [/workspa"}}} -{"t_ms": 4896, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ce/input.txt"}}} -{"t_ms": 4921, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "](file:///tmp"}}} -{"t_ms": 4972, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "/agy-p0/work"}}} -{"t_ms": 4998, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "space/input.txt) are:\n\n```\nPHASE0_RE"}}} -{"t_ms": 5023, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "AD_MARKER\n``"}}} -{"t_ms": 5048, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "`"}}} -{"t_ms": 5150, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 3, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 2.360276704, "usage": {"input_tokens": 11494, "output_tokens": 47, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11541}}}} -{"t_ms": 5150, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "status": "SUCCESS", "response": "The contents of [/input.txt](file:///input.txt) are:\n\n```\nPHASE0_READ_MARKER\n```\n", "duration_seconds": 4.857244426, "num_turns": 1, "usage": {"input_tokens": 22766, "output_tokens": 196, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 22962}}}} -{"t_ms": 5176, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 4, "state": "DONE", "step_type": "user_input"}}} -{"t_ms": 8771, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 5, "state": "DONE", "step_type": "agent_response", "duration_seconds": 3.600197654, "usage": {"input_tokens": 11663, "output_tokens": 419, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12082}}}} -{"t_ms": 8771, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 6, "state": "ACTIVE", "step_type": "tool", "tool_name": "write_to_file", "tool_info": {"name": "write_to_file", "parameters": {"TargetFile": "/accept-edits.txt"}}}}} -{"t_ms": 8771, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 6, "state": "DONE", "step_type": "tool", "tool_name": "write_to_file", "duration_seconds": 0.003051666, "tool_info": {"name": "write_to_file", "parameters": {"TargetFile": "/accept-edits.txt"}}}}} -{"t_ms": 11213, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 7, "state": "DONE", "step_type": "agent_response", "duration_seconds": 2.42873306, "usage": {"input_tokens": 11846, "output_tokens": 129, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11975}}}} -{"t_ms": 11213, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 8, "state": "ACTIVE", "step_type": "tool", "tool_name": "replace_file_content", "tool_info": {"name": "replace_file_content", "parameters": {"TargetFile": "/accept-edits.txt"}}}}} -{"t_ms": 11213, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 8, "state": "DONE", "step_type": "tool", "tool_name": "replace_file_content", "duration_seconds": 0.003025767, "tool_info": {"name": "replace_file_content", "parameters": {"TargetFile": "/accept-edits.txt"}}}}} -{"t_ms": 12799, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "The file"}}} -{"t_ms": 12900, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " [/"}}} -{"t_ms": 12926, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "accept-edits"}}} -{"t_ms": 12951, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": ".txt](file:///"}}} -{"t_ms": 13002, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "tmp/agy-p0/w"}}} -{"t_ms": 13027, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "orkspace/accept-edits.tx"}}} -{"t_ms": 13053, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "t) has been "}}} -{"t_ms": 13078, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "created with `PHASE0"}}} -{"t_ms": 13155, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "_WRITE` and then updated"}}} -{"t_ms": 13180, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " to `PHASE0_"}}} -{"t_ms": 13206, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "EDIT`."}}} -{"t_ms": 13611, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 9, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 2.385314949, "usage": {"input_tokens": 12152, "output_tokens": 59, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12211}}}} -{"t_ms": 13611, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "status": "SUCCESS", "response": "The file [/accept-edits.txt](file:///accept-edits.txt) has been created with `PHASE0_WRITE` and then updated to `PHASE0_EDIT`.\n", "duration_seconds": 13.300599177, "num_turns": 2, "usage": {"input_tokens": 58427, "output_tokens": 803, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 59230}}}} -{"t_ms": 13638, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 10, "state": "DONE", "step_type": "user_input"}}} -{"t_ms": 15928, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 11, "state": "DONE", "step_type": "agent_response", "duration_seconds": 2.302666458, "usage": {"input_tokens": 12310, "output_tokens": 98, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12408}}}} -{"t_ms": 15928, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 12, "state": "ACTIVE", "step_type": "tool", "tool_name": "run_command", "tool_info": {"name": "run_command", "parameters": {"CommandLine": "printf PHASE0_SHELL"}}}}} -{"t_ms": 15954, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 12, "state": "ERROR", "step_type": "tool", "tool_name": "run_command", "duration_seconds": 0.002216645, "tool_info": {"name": "run_command", "parameters": {"CommandLine": "printf PHASE0_SHELL"}, "error": {"type": "TOOL_ERROR", "message": "permission check failed for command \"printf PHASE0_SHELL\": user denied permission to run command:\nprintf PHASE0_SHELL\nDo not attempt to circumvent this denial by rephrasing the command, using alternative tools/scripts (e.g. python, sh, curl), or accessing the same target resource. Proceed without performing this action."}}}}} -{"t_ms": 15954, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "status": "SUCCESS", "response": "", "duration_seconds": 15.639601905, "num_turns": 3, "usage": {"input_tokens": 70737, "output_tokens": 901, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 71638}, "denied_actions": [{"action": "command", "display_name": "RunCommand"}]}}} diff --git a/plugins/antigravity-provider/test/fixtures/tools-default.capture.ndjson b/plugins/antigravity-provider/test/fixtures/tools-default.capture.ndjson deleted file mode 100644 index 8c57e4e549..0000000000 --- a/plugins/antigravity-provider/test/fixtures/tools-default.capture.ndjson +++ /dev/null @@ -1,2 +0,0 @@ -{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", "", "--disable-slash-commands"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} -{"t_ms": 21620, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/tools-default.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/tools-default.stderr.ndjson deleted file mode 100644 index 4e24b2cb59..0000000000 --- a/plugins/antigravity-provider/test/fixtures/tools-default.stderr.ndjson +++ /dev/null @@ -1 +0,0 @@ -{"t_ms": 21540, "stream": "stderr", "data": "jetski: no output produced — a tool required the \"command\" permission that headless mode cannot prompt for, so it was auto-denied. Add an allow-rule under permissions.allow in settings.json (e.g. command()). Alternatively, re-run with --dangerously-skip-permissions to auto-approve all tools."} diff --git a/plugins/antigravity-provider/test/fixtures/tools-default.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/tools-default.stdin.ndjson deleted file mode 100644 index 371a7bda84..0000000000 --- a/plugins/antigravity-provider/test/fixtures/tools-default.stdin.ndjson +++ /dev/null @@ -1,3 +0,0 @@ -{"t_ms": 297, "stream": "stdin", "data": {"event": "user", "message": {"role": "user", "content": "Use view_file to read /input.txt. Report its contents. Do not use a shell."}}} -{"t_ms": 6781, "stream": "stdin", "data": {"event": "user", "message": {"role": "user", "content": "Use write_to_file to create /default.txt with PHASE0_WRITE. Then use replace_file_content to change PHASE0_WRITE to PHASE0_EDIT. No shell. Do not ask me questions."}}} -{"t_ms": 17022, "stream": "stdin", "data": {"event": "user", "message": {"role": "user", "content": "Use run_command exactly once to run printf PHASE0_SHELL in . Report the output. Do not ask me questions."}}} diff --git a/plugins/antigravity-provider/test/fixtures/tools-default.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/tools-default.stdout.ndjson deleted file mode 100644 index dedb01d707..0000000000 --- a/plugins/antigravity-provider/test/fixtures/tools-default.stdout.ndjson +++ /dev/null @@ -1,41 +0,0 @@ -{"t_ms": 297, "stream": "stdout", "data": {"event": "init", "conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "request-review"}}} -{"t_ms": 326, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} -{"t_ms": 4079, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 1, "state": "DONE", "step_type": "agent_response", "duration_seconds": 3.738430576, "usage": {"input_tokens": 11257, "output_tokens": 149, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11406}}}} -{"t_ms": 4079, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 2, "state": "ACTIVE", "step_type": "tool", "tool_name": "view_file", "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/input.txt"}}}}} -{"t_ms": 4079, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 2, "state": "DONE", "step_type": "tool", "tool_name": "view_file", "duration_seconds": 0.002338062, "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/input.txt"}, "output": "2 lines, 19 bytes"}}}} -{"t_ms": 6096, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "The contents"}}} -{"t_ms": 6223, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " of [`/worksp"}}} -{"t_ms": 6248, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ace/input.tx"}}} -{"t_ms": 6274, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "t`](file:///tmp/agy"}}} -{"t_ms": 6350, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "-p0/workspace/input.txt) are:\n\n```te"}}} -{"t_ms": 6375, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "xt\nPHASE0_RE"}}} -{"t_ms": 6400, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "AD_MARKER"}}} -{"t_ms": 6451, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "\n```"}}} -{"t_ms": 6781, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 3, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 2.698156575, "usage": {"input_tokens": 11478, "output_tokens": 49, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11527}}}} -{"t_ms": 6781, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "status": "SUCCESS", "response": "The contents of [`/input.txt`](file:///input.txt) are:\n\n```text\nPHASE0_READ_MARKER\n```\n", "duration_seconds": 6.466229654, "num_turns": 1, "usage": {"input_tokens": 22735, "output_tokens": 198, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 22933}}}} -{"t_ms": 6807, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 4, "state": "DONE", "step_type": "user_input"}}} -{"t_ms": 10766, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 5, "state": "DONE", "step_type": "agent_response", "duration_seconds": 3.966197803, "usage": {"input_tokens": 11646, "output_tokens": 171, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11817}}}} -{"t_ms": 10766, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 6, "state": "ACTIVE", "step_type": "tool", "tool_name": "write_to_file", "tool_info": {"name": "write_to_file", "parameters": {"TargetFile": "/default.txt"}}}}} -{"t_ms": 10791, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 6, "state": "DONE", "step_type": "tool", "tool_name": "write_to_file", "duration_seconds": 0.003040074, "tool_info": {"name": "write_to_file", "parameters": {"TargetFile": "/default.txt"}}}}} -{"t_ms": 13910, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 7, "state": "DONE", "step_type": "agent_response", "duration_seconds": 3.136362179, "usage": {"input_tokens": 11813, "output_tokens": 116, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11929}}}} -{"t_ms": 13911, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 8, "state": "ACTIVE", "step_type": "tool", "tool_name": "replace_file_content", "tool_info": {"name": "replace_file_content", "parameters": {"TargetFile": "/default.txt"}}}}} -{"t_ms": 13911, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 8, "state": "DONE", "step_type": "tool", "tool_name": "replace_file_content", "duration_seconds": 0.003396671, "tool_info": {"name": "replace_file_content", "parameters": {"TargetFile": "/default.txt"}}}}} -{"t_ms": 15904, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "I"}}} -{"t_ms": 16032, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " have create"}}} -{"t_ms": 16057, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "d [`/workspac"}}} -{"t_ms": 16083, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "e/default.txt`](file:///"}}} -{"t_ms": 16159, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "tmp/agy-p0/w"}}} -{"t_ms": 16184, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "orkspace/default.txt) wi"}}} -{"t_ms": 16210, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "th `PHASE0_WRITE` and th"}}} -{"t_ms": 16235, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "en updated i"}}} -{"t_ms": 16261, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "t to"}}} -{"t_ms": 16286, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " `PHASE0_EDI"}}} -{"t_ms": 16312, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "T` using `replace_file_c"}}} -{"t_ms": 16337, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ontent`."}}} -{"t_ms": 17022, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 9, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 3.099484037, "usage": {"input_tokens": 12103, "output_tokens": 61, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12164}}}} -{"t_ms": 17022, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "status": "SUCCESS", "response": "I have created [`/default.txt`](file:///default.txt) with `PHASE0_WRITE` and then updated it to `PHASE0_EDIT` using `replace_file_content`.\n", "duration_seconds": 16.710782303, "num_turns": 2, "usage": {"input_tokens": 58297, "output_tokens": 546, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 58843}}}} -{"t_ms": 17049, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 10, "state": "DONE", "step_type": "user_input"}}} -{"t_ms": 21540, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 11, "state": "DONE", "step_type": "agent_response", "duration_seconds": 4.490021482, "usage": {"input_tokens": 12263, "output_tokens": 370, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12633}}}} -{"t_ms": 21540, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 12, "state": "ACTIVE", "step_type": "tool", "tool_name": "run_command", "tool_info": {"name": "run_command", "parameters": {"CommandLine": "printf PHASE0_SHELL"}}}}} -{"t_ms": 21540, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 12, "state": "ERROR", "step_type": "tool", "tool_name": "run_command", "duration_seconds": 0.002821636, "tool_info": {"name": "run_command", "parameters": {"CommandLine": "printf PHASE0_SHELL"}, "error": {"type": "TOOL_ERROR", "message": "permission check failed for command \"printf PHASE0_SHELL\": user denied permission to run command:\nprintf PHASE0_SHELL\nDo not attempt to circumvent this denial by rephrasing the command, using alternative tools/scripts (e.g. python, sh, curl), or accessing the same target resource. Proceed without performing this action."}}}}} -{"t_ms": 21540, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "status": "SUCCESS", "response": "", "duration_seconds": 21.23332446, "num_turns": 3, "usage": {"input_tokens": 70560, "output_tokens": 916, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 71476}, "denied_actions": [{"action": "command", "display_name": "RunCommand"}]}}} diff --git a/plugins/antigravity-provider/test/fixtures/tools-plan.capture.ndjson b/plugins/antigravity-provider/test/fixtures/tools-plan.capture.ndjson deleted file mode 100644 index 4a1151d083..0000000000 --- a/plugins/antigravity-provider/test/fixtures/tools-plan.capture.ndjson +++ /dev/null @@ -1,2 +0,0 @@ -{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", "", "--disable-slash-commands", "--mode", "plan"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} -{"t_ms": 16560, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/tools-plan.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/tools-plan.stderr.ndjson deleted file mode 100644 index be57aac639..0000000000 --- a/plugins/antigravity-provider/test/fixtures/tools-plan.stderr.ndjson +++ /dev/null @@ -1,4 +0,0 @@ -{"t_ms": 292, "stream": "stderr", "data": "warning: --mode plan has no effect while slash command expansion is disabled."} -{"t_ms": 6705, "stream": "stderr", "data": "warning: --mode plan has no effect while slash command expansion is disabled."} -{"t_ms": 13456, "stream": "stderr", "data": "warning: --mode plan has no effect while slash command expansion is disabled."} -{"t_ms": 16400, "stream": "stderr", "data": "jetski: no output produced — a tool required the \"command\" permission that headless mode cannot prompt for, so it was auto-denied. Add an allow-rule under permissions.allow in settings.json (e.g. command()). Alternatively, re-run with --dangerously-skip-permissions to auto-approve all tools."} diff --git a/plugins/antigravity-provider/test/fixtures/tools-plan.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/tools-plan.stdin.ndjson deleted file mode 100644 index c4d09c8ae7..0000000000 --- a/plugins/antigravity-provider/test/fixtures/tools-plan.stdin.ndjson +++ /dev/null @@ -1,3 +0,0 @@ -{"t_ms": 292, "stream": "stdin", "data": {"event": "user", "message": {"role": "user", "content": "Use view_file to read /input.txt. Report its contents. Do not use a shell."}}} -{"t_ms": 6705, "stream": "stdin", "data": {"event": "user", "message": {"role": "user", "content": "Use write_to_file to create /plan.txt with PHASE0_WRITE. Then use replace_file_content to change PHASE0_WRITE to PHASE0_EDIT. No shell. Do not ask me questions."}}} -{"t_ms": 13455, "stream": "stdin", "data": {"event": "user", "message": {"role": "user", "content": "Use run_command exactly once to run printf PHASE0_SHELL in . Report the output. Do not ask me questions."}}} diff --git a/plugins/antigravity-provider/test/fixtures/tools-plan.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/tools-plan.stdout.ndjson deleted file mode 100644 index 473540451c..0000000000 --- a/plugins/antigravity-provider/test/fixtures/tools-plan.stdout.ndjson +++ /dev/null @@ -1,36 +0,0 @@ -{"t_ms": 292, "stream": "stdout", "data": {"event": "init", "conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "request-review"}}} -{"t_ms": 319, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} -{"t_ms": 3576, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 1, "state": "DONE", "step_type": "agent_response", "duration_seconds": 3.249717435, "usage": {"input_tokens": 11253, "output_tokens": 339, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11592}}}} -{"t_ms": 3576, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 2, "state": "ACTIVE", "step_type": "tool", "tool_name": "view_file", "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/input.txt"}}}}} -{"t_ms": 3576, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 2, "state": "DONE", "step_type": "tool", "tool_name": "view_file", "duration_seconds": 0.002425685, "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/input.txt"}, "output": "2 lines, 19 bytes"}}}} -{"t_ms": 6197, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "The contents"}}} -{"t_ms": 6324, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " of [`/input.tx"}}} -{"t_ms": 6349, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "t`](file:///"}}} -{"t_ms": 6374, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "tmp/"}}} -{"t_ms": 6451, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "agy-p0/workspace/input.txt) are:\n\n``"}}} -{"t_ms": 6476, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "`\nPHASE0_REA"}}} -{"t_ms": 6502, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "D_MARKER\n```"}}} -{"t_ms": 6705, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 3, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 3.109263873, "usage": {"input_tokens": 11475, "output_tokens": 48, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11523}}}} -{"t_ms": 6705, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "status": "SUCCESS", "response": "The contents of [`/input.txt`](file:///input.txt) are:\n\n```\nPHASE0_READ_MARKER\n```\n", "duration_seconds": 6.388206511, "num_turns": 1, "usage": {"input_tokens": 22728, "output_tokens": 387, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 23115}}}} -{"t_ms": 6732, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 4, "state": "DONE", "step_type": "user_input"}}} -{"t_ms": 9502, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 5, "state": "DONE", "step_type": "agent_response", "duration_seconds": 2.757462001, "usage": {"input_tokens": 11642, "output_tokens": 166, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11808}}}} -{"t_ms": 9502, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 6, "state": "ACTIVE", "step_type": "tool", "tool_name": "write_to_file", "tool_info": {"name": "write_to_file", "parameters": {"TargetFile": "/plan.txt"}}}}} -{"t_ms": 9502, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 6, "state": "DONE", "step_type": "tool", "tool_name": "write_to_file", "duration_seconds": 0.003883221, "tool_info": {"name": "write_to_file", "parameters": {"TargetFile": "/plan.txt"}}}}} -{"t_ms": 11716, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 7, "state": "DONE", "step_type": "agent_response", "duration_seconds": 2.227570207, "usage": {"input_tokens": 11811, "output_tokens": 118, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11929}}}} -{"t_ms": 11716, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 8, "state": "ACTIVE", "step_type": "tool", "tool_name": "replace_file_content", "tool_info": {"name": "replace_file_content", "parameters": {"TargetFile": "/plan.txt"}}}}} -{"t_ms": 11742, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 8, "state": "DONE", "step_type": "tool", "tool_name": "replace_file_content", "duration_seconds": 0.002974983, "tool_info": {"name": "replace_file_content", "parameters": {"TargetFile": "/plan.txt"}}}}} -{"t_ms": 13151, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "[`/"}}} -{"t_ms": 13176, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "plan.txt`](file:///tmp/"}}} -{"t_ms": 13252, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "agy-p0/workspace/plan.tx"}}} -{"t_ms": 13278, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "t) was created with `PHA"}}} -{"t_ms": 13303, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "SE0_WRITE` and then upda"}}} -{"t_ms": 13329, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ted to `PHASE0_EDIT` usi"}}} -{"t_ms": 13354, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ng `replace_file_content"}}} -{"t_ms": 13379, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "`."}}} -{"t_ms": 13455, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 9, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 1.716582968, "usage": {"input_tokens": 12103, "output_tokens": 59, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12162}}}} -{"t_ms": 13455, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "status": "SUCCESS", "response": "[`/plan.txt`](file:///plan.txt) was created with `PHASE0_WRITE` and then updated to `PHASE0_EDIT` using `replace_file_content`.\n", "duration_seconds": 13.142130621, "num_turns": 2, "usage": {"input_tokens": 58284, "output_tokens": 730, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 59014}}}} -{"t_ms": 13482, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 10, "state": "DONE", "step_type": "user_input"}}} -{"t_ms": 16400, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 11, "state": "DONE", "step_type": "agent_response", "duration_seconds": 2.913339623, "usage": {"input_tokens": 12261, "output_tokens": 74, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12335}}}} -{"t_ms": 16400, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 12, "state": "ACTIVE", "step_type": "tool", "tool_name": "run_command", "tool_info": {"name": "run_command", "parameters": {"CommandLine": "printf PHASE0_SHELL"}}}}} -{"t_ms": 16400, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 12, "state": "ERROR", "step_type": "tool", "tool_name": "run_command", "duration_seconds": 0.002793073, "tool_info": {"name": "run_command", "parameters": {"CommandLine": "printf PHASE0_SHELL"}, "error": {"type": "TOOL_ERROR", "message": "permission check failed for command \"printf PHASE0_SHELL\": user denied permission to run command:\nprintf PHASE0_SHELL\nDo not attempt to circumvent this denial by rephrasing the command, using alternative tools/scripts (e.g. python, sh, curl), or accessing the same target resource. Proceed without performing this action."}}}}} -{"t_ms": 16400, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "status": "SUCCESS", "response": "", "duration_seconds": 16.097866635, "num_turns": 3, "usage": {"input_tokens": 70545, "output_tokens": 804, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 71349}, "denied_actions": [{"action": "command", "display_name": "RunCommand"}]}}} diff --git a/plugins/antigravity-provider/test/fixtures/tools-sandbox.capture.ndjson b/plugins/antigravity-provider/test/fixtures/tools-sandbox.capture.ndjson deleted file mode 100644 index b845d3e735..0000000000 --- a/plugins/antigravity-provider/test/fixtures/tools-sandbox.capture.ndjson +++ /dev/null @@ -1,2 +0,0 @@ -{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", "", "--disable-slash-commands", "--sandbox"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} -{"t_ms": 13022, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/tools-sandbox.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/tools-sandbox.stderr.ndjson deleted file mode 100644 index 624a99a74c..0000000000 --- a/plugins/antigravity-provider/test/fixtures/tools-sandbox.stderr.ndjson +++ /dev/null @@ -1 +0,0 @@ -{"t_ms": 12723, "stream": "stderr", "data": "jetski: no output produced — a tool required the \"command\" permission that headless mode cannot prompt for, so it was auto-denied. Add an allow-rule under permissions.allow in settings.json (e.g. command()). Alternatively, re-run with --dangerously-skip-permissions to auto-approve all tools."} diff --git a/plugins/antigravity-provider/test/fixtures/tools-sandbox.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/tools-sandbox.stdin.ndjson deleted file mode 100644 index 9ebac338e8..0000000000 --- a/plugins/antigravity-provider/test/fixtures/tools-sandbox.stdin.ndjson +++ /dev/null @@ -1,3 +0,0 @@ -{"t_ms": 320, "stream": "stdin", "data": {"event": "user", "message": {"role": "user", "content": "Use view_file to read /input.txt. Report its contents. Do not use a shell."}}} -{"t_ms": 4959, "stream": "stdin", "data": {"event": "user", "message": {"role": "user", "content": "Use write_to_file to create /sandbox.txt with PHASE0_WRITE. Then use replace_file_content to change PHASE0_WRITE to PHASE0_EDIT. No shell. Do not ask me questions."}}} -{"t_ms": 10581, "stream": "stdin", "data": {"event": "user", "message": {"role": "user", "content": "Use run_command exactly once to run printf PHASE0_SHELL in . Report the output. Do not ask me questions."}}} diff --git a/plugins/antigravity-provider/test/fixtures/tools-sandbox.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/tools-sandbox.stdout.ndjson deleted file mode 100644 index 6d63907468..0000000000 --- a/plugins/antigravity-provider/test/fixtures/tools-sandbox.stdout.ndjson +++ /dev/null @@ -1,40 +0,0 @@ -{"t_ms": 319, "stream": "stdout", "data": {"event": "init", "conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "request-review"}}} -{"t_ms": 346, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} -{"t_ms": 3169, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 1, "state": "DONE", "step_type": "agent_response", "duration_seconds": 2.806171145, "usage": {"input_tokens": 12031, "output_tokens": 307, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12338}}}} -{"t_ms": 3169, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 2, "state": "ACTIVE", "step_type": "tool", "tool_name": "view_file", "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/input.txt"}}}}} -{"t_ms": 3169, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 2, "state": "DONE", "step_type": "tool", "tool_name": "view_file", "duration_seconds": 0.002973019, "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/input.txt"}, "output": "2 lines, 19 bytes"}}}} -{"t_ms": 4705, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "The contents"}}} -{"t_ms": 4755, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " of [input.txt](file:///"}}} -{"t_ms": 4781, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "tmp/agy-p0/w"}}} -{"t_ms": 4806, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "orkspace/input"}}} -{"t_ms": 4857, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": ".txt) are:\n\n"}}} -{"t_ms": 4882, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "```text\nPHASE0_READ_MARK"}}} -{"t_ms": 4908, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ER\n```"}}} -{"t_ms": 4959, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 3, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 1.791180207, "usage": {"input_tokens": 12253, "output_tokens": 39, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12292}}}} -{"t_ms": 4959, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "status": "SUCCESS", "response": "The contents of [input.txt](file:///input.txt) are:\n\n```text\nPHASE0_READ_MARKER\n```\n", "duration_seconds": 4.624371092, "num_turns": 1, "usage": {"input_tokens": 24284, "output_tokens": 346, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 24630}}}} -{"t_ms": 4986, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 4, "state": "DONE", "step_type": "user_input"}}} -{"t_ms": 6674, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 5, "state": "DONE", "step_type": "agent_response", "duration_seconds": 1.678518989, "usage": {"input_tokens": 12411, "output_tokens": 78, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12489}}}} -{"t_ms": 6674, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 6, "state": "ACTIVE", "step_type": "tool", "tool_name": "write_to_file", "tool_info": {"name": "write_to_file", "parameters": {"TargetFile": "/sandbox.txt"}}}}} -{"t_ms": 6674, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 6, "state": "DONE", "step_type": "tool", "tool_name": "write_to_file", "duration_seconds": 0.002765401, "tool_info": {"name": "write_to_file", "parameters": {"TargetFile": "/sandbox.txt"}}}}} -{"t_ms": 8939, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 7, "state": "DONE", "step_type": "agent_response", "duration_seconds": 2.278065329, "usage": {"input_tokens": 12577, "output_tokens": 118, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12695}}}} -{"t_ms": 8939, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 8, "state": "ACTIVE", "step_type": "tool", "tool_name": "replace_file_content", "tool_info": {"name": "replace_file_content", "parameters": {"TargetFile": "/sandbox.txt"}}}}} -{"t_ms": 8965, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 8, "state": "DONE", "step_type": "tool", "tool_name": "replace_file_content", "duration_seconds": 0.002992846, "tool_info": {"name": "replace_file_content", "parameters": {"TargetFile": "/sandbox.txt"}}}}} -{"t_ms": 10124, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "### Summary"}}} -{"t_ms": 10251, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " of Changes\n\n1. **Created File**: Us"}}} -{"t_ms": 10276, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ed `write_to"}}} -{"t_ms": 10302, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "_file` to create"}}} -{"t_ms": 10378, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " [sandbox.tx"}}} -{"t_ms": 10403, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "t](file:///wo"}}} -{"t_ms": 10428, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "rkspace/sandbox.txt) wit"}}} -{"t_ms": 10454, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "h the conten"}}} -{"t_ms": 10479, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "t `PHASE0_WRITE`.\n2. **Updated Con"}}} -{"t_ms": 10504, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "tent**: Used `replace_fi"}}} -{"t_ms": 10530, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "le_content` to replace `"}}} -{"t_ms": 10555, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "PHASE0_WRITE` with `PHAS"}}} -{"t_ms": 10581, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 9, "state": "DONE", "step_type": "agent_response", "text_delta": "E0_EDIT`.\n", "duration_seconds": 1.630993868, "usage": {"input_tokens": 12869, "output_tokens": 83, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12952}}}} -{"t_ms": 10581, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "status": "SUCCESS", "response": "### Summary of Changes\n\n1. **Created File**: Used `write_to_file` to create [sandbox.txt](file:///sandbox.txt) with the content `PHASE0_WRITE`.\n2. **Updated Content**: Used `replace_file_content` to replace `PHASE0_WRITE` with `PHASE0_EDIT`.\n", "duration_seconds": 10.25241531, "num_turns": 2, "usage": {"input_tokens": 62141, "output_tokens": 625, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 62766}}}} -{"t_ms": 10608, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 10, "state": "DONE", "step_type": "user_input"}}} -{"t_ms": 12723, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 11, "state": "DONE", "step_type": "agent_response", "duration_seconds": 2.117119481, "usage": {"input_tokens": 13051, "output_tokens": 80, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 13131}}}} -{"t_ms": 12723, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 12, "state": "ACTIVE", "step_type": "tool", "tool_name": "run_command", "tool_info": {"name": "run_command", "parameters": {"CommandLine": "printf PHASE0_SHELL"}}}}} -{"t_ms": 12723, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 12, "state": "ERROR", "step_type": "tool", "tool_name": "run_command", "duration_seconds": 0.00280781, "tool_info": {"name": "run_command", "parameters": {"CommandLine": "printf PHASE0_SHELL"}, "error": {"type": "TOOL_ERROR", "message": "permission check failed for command \"printf PHASE0_SHELL\": user denied permission to run command:\nprintf PHASE0_SHELL\nDo not attempt to circumvent this denial by rephrasing the command, using alternative tools/scripts (e.g. python, sh, curl), or accessing the same target resource. Proceed without performing this action."}}}}} -{"t_ms": 12723, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "status": "SUCCESS", "response": "", "duration_seconds": 12.395964579, "num_turns": 3, "usage": {"input_tokens": 75192, "output_tokens": 705, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 75897}, "denied_actions": [{"action": "command", "display_name": "RunCommand"}]}}} diff --git a/plugins/antigravity-provider/test/fixtures/tools-skip-permissions.capture.ndjson b/plugins/antigravity-provider/test/fixtures/tools-skip-permissions.capture.ndjson deleted file mode 100644 index dd49b19784..0000000000 --- a/plugins/antigravity-provider/test/fixtures/tools-skip-permissions.capture.ndjson +++ /dev/null @@ -1,2 +0,0 @@ -{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", "", "--disable-slash-commands", "--dangerously-skip-permissions"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} -{"t_ms": 17771, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/tools-skip-permissions.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/tools-skip-permissions.stderr.ndjson deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/plugins/antigravity-provider/test/fixtures/tools-skip-permissions.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/tools-skip-permissions.stdin.ndjson deleted file mode 100644 index f91311bcf7..0000000000 --- a/plugins/antigravity-provider/test/fixtures/tools-skip-permissions.stdin.ndjson +++ /dev/null @@ -1,3 +0,0 @@ -{"t_ms": 292, "stream": "stdin", "data": {"event": "user", "message": {"role": "user", "content": "Use view_file to read /input.txt. Report its contents. Do not use a shell."}}} -{"t_ms": 5051, "stream": "stdin", "data": {"event": "user", "message": {"role": "user", "content": "Use write_to_file to create /skip-permissions.txt with PHASE0_WRITE. Then use replace_file_content to change PHASE0_WRITE to PHASE0_EDIT. No shell. Do not ask me questions."}}} -{"t_ms": 11704, "stream": "stdin", "data": {"event": "user", "message": {"role": "user", "content": "Use run_command exactly once to run printf PHASE0_SHELL in . Report the output. Do not ask me questions."}}} diff --git a/plugins/antigravity-provider/test/fixtures/tools-skip-permissions.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/tools-skip-permissions.stdout.ndjson deleted file mode 100644 index 9d2f1fbafa..0000000000 --- a/plugins/antigravity-provider/test/fixtures/tools-skip-permissions.stdout.ndjson +++ /dev/null @@ -1,42 +0,0 @@ -{"t_ms": 291, "stream": "stdout", "data": {"event": "init", "conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "always-proceed"}}} -{"t_ms": 318, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} -{"t_ms": 2658, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 1, "state": "DONE", "step_type": "agent_response", "duration_seconds": 2.317552401, "usage": {"input_tokens": 11263, "output_tokens": 150, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11413}}}} -{"t_ms": 2658, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 2, "state": "ACTIVE", "step_type": "tool", "tool_name": "view_file", "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/input.txt"}}}}} -{"t_ms": 2658, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 2, "state": "DONE", "step_type": "tool", "tool_name": "view_file", "duration_seconds": 0.002646148, "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/input.txt"}, "output": "2 lines, 19 bytes"}}}} -{"t_ms": 4645, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "The contents"}}} -{"t_ms": 4721, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " of [input.txt](file:///"}}} -{"t_ms": 4746, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "tmp/agy-p0/workspace/inp"}}} -{"t_ms": 4772, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ut"}}} -{"t_ms": 4848, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": ".txt) are:\n\n```\nPHASE0_READ_MARKER\n`"}}} -{"t_ms": 4873, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "``"}}} -{"t_ms": 5025, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 3, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 2.386909321, "usage": {"input_tokens": 11484, "output_tokens": 38, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11522}}}} -{"t_ms": 5051, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "status": "SUCCESS", "response": "The contents of [input.txt](file:///input.txt) are:\n\n```\nPHASE0_READ_MARKER\n```\n", "duration_seconds": 4.732286256, "num_turns": 1, "usage": {"input_tokens": 22747, "output_tokens": 188, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 22935}}}} -{"t_ms": 5078, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 4, "state": "DONE", "step_type": "user_input"}}} -{"t_ms": 7346, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 5, "state": "DONE", "step_type": "agent_response", "duration_seconds": 2.260780071, "usage": {"input_tokens": 11643, "output_tokens": 152, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11795}}}} -{"t_ms": 7346, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 6, "state": "ACTIVE", "step_type": "tool", "tool_name": "write_to_file", "tool_info": {"name": "write_to_file", "parameters": {"TargetFile": "/skip-permissions.txt"}}}}} -{"t_ms": 7346, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 6, "state": "DONE", "step_type": "tool", "tool_name": "write_to_file", "duration_seconds": 0.003788063, "tool_info": {"name": "write_to_file", "parameters": {"TargetFile": "/skip-permissions.txt"}}}}} -{"t_ms": 10113, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 7, "state": "DONE", "step_type": "agent_response", "duration_seconds": 2.769162417, "usage": {"input_tokens": 11812, "output_tokens": 117, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11929}}}} -{"t_ms": 10113, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 8, "state": "ACTIVE", "step_type": "tool", "tool_name": "replace_file_content", "tool_info": {"name": "replace_file_content", "parameters": {"TargetFile": "/skip-permissions.txt"}}}}} -{"t_ms": 10113, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 8, "state": "DONE", "step_type": "tool", "tool_name": "replace_file_content", "duration_seconds": 0.004067275, "tool_info": {"name": "replace_file_content", "parameters": {"TargetFile": "/skip-permissions.txt"}}}}} -{"t_ms": 11349, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "I have"}}} -{"t_ms": 11450, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " created [skip-permissio"}}} -{"t_ms": 11476, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ns.txt](file"}}} -{"t_ms": 11501, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "://"}}} -{"t_ms": 11526, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "/skip"}}} -{"t_ms": 11552, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "-permissions.txt) with `"}}} -{"t_ms": 11577, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "PHASE0_WRITE` and then u"}}} -{"t_ms": 11603, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "pdated its c"}}} -{"t_ms": 11628, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ontent to `PHASE0_EDIT` using `repla"}}} -{"t_ms": 11653, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ce_file_cont"}}} -{"t_ms": 11678, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ent`."}}} -{"t_ms": 11704, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 9, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 1.563629905, "usage": {"input_tokens": 12105, "output_tokens": 56, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12161}}}} -{"t_ms": 11704, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "status": "SUCCESS", "response": "I have created [skip-permissions.txt](file:///skip-permissions.txt) with `PHASE0_WRITE` and then updated its content to `PHASE0_EDIT` using `replace_file_content`.\n", "duration_seconds": 11.386053268, "num_turns": 2, "usage": {"input_tokens": 58307, "output_tokens": 513, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 58820}}}} -{"t_ms": 11731, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 10, "state": "DONE", "step_type": "user_input"}}} -{"t_ms": 16142, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 11, "state": "DONE", "step_type": "agent_response", "duration_seconds": 4.411191659, "usage": {"input_tokens": 12260, "output_tokens": 390, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12650}}}} -{"t_ms": 16143, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 12, "state": "ACTIVE", "step_type": "tool", "tool_name": "run_command", "tool_info": {"name": "run_command", "parameters": {"CommandLine": "printf PHASE0_SHELL"}}}}} -{"t_ms": 16143, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 12, "state": "DONE", "step_type": "tool", "tool_name": "run_command", "duration_seconds": 0.007492259, "tool_info": {"name": "run_command", "parameters": {"CommandLine": "printf PHASE0_SHELL"}, "output": "PHASE0_SHELL"}}}} -{"t_ms": 17501, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 13, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "The command"}}} -{"t_ms": 17578, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 13, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " output is:\n"}}} -{"t_ms": 17603, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 13, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "\n```\nPHASE0_SHELL\n```"}}} -{"t_ms": 17680, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 13, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 1.518696431, "usage": {"input_tokens": 12396, "output_tokens": 16, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12412}}}} -{"t_ms": 17680, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "status": "SUCCESS", "response": "The command output is:\n\n```\nPHASE0_SHELL\n```\n", "duration_seconds": 17.367491474, "num_turns": 3, "usage": {"input_tokens": 82963, "output_tokens": 919, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 83882}}}} diff --git a/plugins/antigravity-provider/test/fixtures/version.capture.ndjson b/plugins/antigravity-provider/test/fixtures/version.capture.ndjson deleted file mode 100644 index ac8e605e77..0000000000 --- a/plugins/antigravity-provider/test/fixtures/version.capture.ndjson +++ /dev/null @@ -1,2 +0,0 @@ -{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--version"], "cwd": "", "home": "", "settings": null, "env": {}}} -{"t_ms": 284, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/version.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/version.stderr.ndjson deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/plugins/antigravity-provider/test/fixtures/version.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/version.stdin.ndjson deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/plugins/antigravity-provider/test/fixtures/version.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/version.stdout.ndjson deleted file mode 100644 index ce8e23e783..0000000000 --- a/plugins/antigravity-provider/test/fixtures/version.stdout.ndjson +++ /dev/null @@ -1 +0,0 @@ -{"t_ms": 269, "stream": "stdout", "data": "1.2.13"} diff --git a/public-docs/providers.md b/public-docs/providers.md index 6de304a019..bf9e82a22c 100644 --- a/public-docs/providers.md +++ b/public-docs/providers.md @@ -8,7 +8,7 @@ category: Providers # Providers -Paseo doesn't ship its own coding agent. It launches and supervises **existing CLIs you've already installed and authenticated**, Claude Code, Codex, OpenCode, Antigravity, Muse Code, Cursor, Gemini, and the rest. Your subscriptions, your config, your skills, your MCP servers all stay intact. Paseo just gives you a UI, a CLI, a relay, and orchestration on top. +Paseo doesn't ship its own coding agent. It launches and supervises **existing CLIs you've already installed and authenticated**, Claude Code, Codex, OpenCode, Muse Code, Cursor, Gemini, and the rest. Your subscriptions, your config, your skills, your MCP servers all stay intact. Paseo just gives you a UI, a CLI, a relay, and orchestration on top. ## Mental model @@ -16,7 +16,7 @@ A provider is the contract between Paseo and one external agent CLI: how to laun ## Two tiers -- **Native support**, Paseo ships a bundled adapter for the major agents (Claude Code, Codex, OpenCode, Pi, [Antigravity](/docs/supported-providers#antigravity), [Muse Code](/docs/muse-code)). Auto-discovered when the underlying CLI is installed, with mode metadata and voice support where applicable. +- **Native support**, Paseo ships a bundled adapter for the major agents (Claude Code, Codex, OpenCode, Pi, [Muse Code](/docs/muse-code)). Auto-discovered when the underlying CLI is installed, with mode metadata and voice support where applicable. - **ACP catalog**, any agent speaking the [Agent Client Protocol](https://agentclientprotocol.com) is supported through a generic adapter. Paseo ships a curated catalog of one-click installs (Cursor, Gemini, GitHub Copilot, Hermes, Kimi, Qwen Code, and 25+ more), and you can add any other ACP agent yourself. Either way, **you install the underlying CLI**. Paseo runs it. diff --git a/public-docs/supported-providers.md b/public-docs/supported-providers.md index aaed86f5c4..98db77d07e 100644 --- a/public-docs/supported-providers.md +++ b/public-docs/supported-providers.md @@ -18,39 +18,8 @@ Work out of the box once the underlying CLI is installed and authenticated. - [Codex](/docs/codex). OpenAI's workspace agent with sandbox controls and optional network access. - [OpenCode](https://opencode.ai/). Open-source coding assistant with multi-provider model support. - [Pi](https://pi.dev). Minimal terminal-based coding agent with multi-provider LLM support. -- [Antigravity](#antigravity). Uses your installed `agy` CLI through a built-in provider plugin. - [Muse Code](/docs/muse-code). Meta's terminal coding agent, bundled as a provider plugin. -### Antigravity - -Install the Antigravity CLI (`agy`) on your daemon host, then run `agy` once to sign in. Pick -**Antigravity** in Paseo. The provider ships with Paseo; no separate plugin install is required. - -**Default**, **Accept edits**, and **Plan** allow workspace edits and deny shell commands. -**Full access** allows everything. Plan does not enforce read-only access. To run a denied shell -command, choose Full access and send another prompt. - -Paseo MCP tools and steering are unavailable. Your own `agy` MCP servers still work. Attached -images are passed as file references for the agent to read. - -If `agy` is outside your daemon's PATH, override its command in `~/.paseo/config.json`. Use `env` -for provider-specific environment variables: - -```json -{ - "agents": { - "providers": { - "antigravity": { - "command": ["/absolute/path/to/agy"], - "env": { "EXAMPLE_VARIABLE": "value" } - } - } - } -} -``` - -See [Custom providers](/docs/custom-providers) for command and environment overrides. - ## ACP catalog Pick any of these from the in-app provider catalog. Each entry is a one-click install of the underlying CLI. From 7a64eef491e059c8daec79e52015882c15a14b50 Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Wed, 30 Sep 2026 20:50:24 +0200 Subject: [PATCH 030/100] Show the command on Codex terminal rows (#5774) * fix(codex): label terminal writes with the command Codex started Codex app-server reports the PTY process id on the commandExecution item, and no longer sends the legacy exec_command_end event that carried it as text. Record the process-to-command label from the item so terminal writes show which command they went to. * ci: rerun after website e2e server start timeout * fix(codex): unwrap the shell from terminal labels like shell rows Terminal labels now use the same command normalization as the shell row, so a Linux wrapper such as /usr/bin/zsh -lc 'python3 -i' labels the terminal as python3 -i. --------- Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> --- .../providers/codex-app-server-agent.test.ts | 45 +++++++++++++++++++ .../agent/providers/codex-app-server-agent.ts | 28 +++++++----- .../codex/test-utils/fake-app-server.ts | 21 +++++++++ .../agent/providers/codex/tool-call-mapper.ts | 2 +- 4 files changed, 83 insertions(+), 13 deletions(-) diff --git a/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts b/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts index bf3ab15c62..fb27b68e96 100644 --- a/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts +++ b/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts @@ -1595,6 +1595,51 @@ describe("Codex app-server provider", () => { } }); + test.each(["python3 -i", "/usr/bin/zsh -lc 'python3 -i'"])( + "labels a terminal write started as %s with the command that started the terminal", + async (command) => { + const appServer = createFakeCodexAppServer(); + const session = new CodexAppServerAgentSession( + createConfig({ cwd: "/workspace/project" }), + null, + createTestLogger(), + async () => appServer.child, + ); + + try { + await session.connect(); + appServer.startsTerminalCommand({ + threadId: "thread-1", + turnId: "turn-1", + itemId: "interactive-shell", + processId: "73", + command, + }); + + const terminalWrite = waitForTimelineToolCall(session, "terminal-session-73-1"); + appServer.typesIntoTerminal({ + threadId: "thread-1", + turnId: "turn-1", + itemId: "interactive-shell", + processId: "73", + text: "print(1)\n", + }); + + await expect(terminalWrite).resolves.toMatchObject({ + item: { + callId: "terminal-session-73-1", + name: "terminal", + detail: { type: "plain_text", label: "python3 -i", text: "print(1)\n" }, + metadata: { processId: "73" }, + }, + }); + appServer.assertNoErrors(); + } finally { + await session.close(); + } + }, + ); + test("keeps repeated writes to one terminal as separate timeline rows", async () => { const appServer = createFakeCodexAppServer(); const session = new CodexAppServerAgentSession( diff --git a/packages/server/src/server/agent/providers/codex-app-server-agent.ts b/packages/server/src/server/agent/providers/codex-app-server-agent.ts index 81ea190454..f15c5a60ad 100644 --- a/packages/server/src/server/agent/providers/codex-app-server-agent.ts +++ b/packages/server/src/server/agent/providers/codex-app-server-agent.ts @@ -57,6 +57,7 @@ import { CodexAsyncQuestions, codexAsyncQuestionToTimeline } from "./codex/async import { mapCodexToolCallEnvelope, mapCodexToolCallFromThreadItem, + normalizeCommandExecutionCommand, splitCodexMcpToolResultImages, } from "./codex/tool-call-mapper.js"; import { @@ -6731,6 +6732,7 @@ export class CodexAppServerAgentSession implements AgentSession { ); const itemId = parsed.item.id; if (normalizedItemType === "commandExecution") { + this.rememberTerminalProcessForItem(parsed.item); const callId = timelineItem.callId || itemId; if (callId && this.emittedExecCommandStartedCallIds.has(callId)) { return; @@ -6850,19 +6852,21 @@ export class CodexAppServerAgentSession implements AgentSession { } private rememberTerminalProcessForCommand(command: unknown, output: string | null): void { - const normalizedCommand = normalizeCodexCommandValue(command); - if (!normalizedCommand) { - return; - } - const displayCommand = - typeof normalizedCommand === "string" - ? normalizedCommand - : normalizedCommand.join(" ").trim(); - if (!displayCommand) { - return; - } const processId = extractCodexTerminalSessionId(output ?? undefined); - if (!processId) { + if (processId) { + this.rememberTerminalProcess(processId, command); + } + } + + private rememberTerminalProcessForItem(item: { [key: string]: unknown }): void { + if (typeof item.processId === "string" && item.processId) { + this.rememberTerminalProcess(item.processId, item.command); + } + } + + private rememberTerminalProcess(processId: string, command: unknown): void { + const displayCommand = normalizeCommandExecutionCommand(command); + if (!displayCommand) { return; } this.terminalCommandByProcessId.set(processId, displayCommand); diff --git a/packages/server/src/server/agent/providers/codex/test-utils/fake-app-server.ts b/packages/server/src/server/agent/providers/codex/test-utils/fake-app-server.ts index 713d744309..226eeb9738 100644 --- a/packages/server/src/server/agent/providers/codex/test-utils/fake-app-server.ts +++ b/packages/server/src/server/agent/providers/codex/test-utils/fake-app-server.ts @@ -25,6 +25,13 @@ interface FakeSilentCommand { command: string; cwd: string; } +interface FakeTerminalCommand { + threadId: string; + turnId: string; + itemId: string; + processId: string; + command: string; +} interface FakeTerminalInput { threadId: string; turnId: string; @@ -81,6 +88,7 @@ export interface FakeCodexAppServer { completesCommand(params: FakeLegacyCommand): void; completesSilentCommand(params: FakeSilentCommand): void; completesSilentLegacyCommand(params: FakeSilentCommand): void; + startsTerminalCommand(params: FakeTerminalCommand): void; typesIntoTerminal(params: FakeTerminalInput): void; says(params: { threadId: string; itemId?: string; text: string; chunks?: string[] }): void; requestCommandApproval(params: { @@ -487,6 +495,19 @@ export function createFakeCodexAppServer( success: true, }); }, + startsTerminalCommand(params) { + writeNotification("item/started", { + threadId: params.threadId, + turnId: params.turnId, + item: { + type: "commandExecution", + id: params.itemId, + status: "inProgress", + command: params.command, + processId: params.processId, + }, + }); + }, typesIntoTerminal(params) { writeNotification("item/commandExecution/terminalInteraction", { threadId: params.threadId, diff --git a/packages/server/src/server/agent/providers/codex/tool-call-mapper.ts b/packages/server/src/server/agent/providers/codex/tool-call-mapper.ts index 2f274d2d3f..64629bc5d9 100644 --- a/packages/server/src/server/agent/providers/codex/tool-call-mapper.ts +++ b/packages/server/src/server/agent/providers/codex/tool-call-mapper.ts @@ -245,7 +245,7 @@ function isWindowsShellCommand(command: string): boolean { return /(?:^|\\)(?:pwsh|powershell|cmd)(?:\.exe)?$/i.test(normalized); } -function normalizeCommandExecutionCommand(value: unknown): string | undefined { +export function normalizeCommandExecutionCommand(value: unknown): string | undefined { if (typeof value === "string") { const normalized = unwrapShellCommand(value); return normalized.length > 0 ? normalized : undefined; From 695ecb281290173bfc7e784f548a01b7cc55bc2f Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Wed, 30 Sep 2026 20:09:10 +0100 Subject: [PATCH 031/100] fix(muse): make package installs and project rules work by default (#5775) * fix(muse): make launch defaults usable and configure each agent Disable sandboxing and trust workspaces by default so package installs and project rules work. Replace the host settings UI with validated per-agent providerOptions, preserving posture across session resume. * docs(muse): describe shared provider options configuration --- docs/plugins.md | 2 +- .../server/daemon-e2e/muse.real.e2e.test.ts | 6 +- plugins/muse-provider/client/settings.tsx | 103 ---------------- plugins/muse-provider/index.client.tsx | 20 ---- plugins/muse-provider/index.server.ts | 9 +- plugins/muse-provider/server/options.ts | 32 +++++ plugins/muse-provider/server/provider.ts | 13 +- plugins/muse-provider/server/settings.ts | 17 --- plugins/muse-provider/shared/settings.ts | 24 ---- plugins/muse-provider/test/provider.test.ts | 112 ++++++++++++------ public-docs/muse-code.md | 47 ++++++-- 11 files changed, 156 insertions(+), 229 deletions(-) delete mode 100644 plugins/muse-provider/client/settings.tsx delete mode 100644 plugins/muse-provider/index.client.tsx create mode 100644 plugins/muse-provider/server/options.ts delete mode 100644 plugins/muse-provider/server/settings.ts delete mode 100644 plugins/muse-provider/shared/settings.ts diff --git a/docs/plugins.md b/docs/plugins.md index a739d417a0..91c0841ffe 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -104,7 +104,7 @@ availability and a diagnostic after the daemon resolves the executable. Configur environment, or enablement overrides under `agents.providers.muse`. Omit `extends` to keep the bundled integration; an entry with `extends` shadows it with a custom provider. See [provider contributions](#contribute-a-provider) for the contract and -[Muse Code](../public-docs/muse-code.md) for setup, settings, and version limitations. +[Muse Code](../public-docs/muse-code.md) for setup, per-agent options, and version limitations. ## Install a Git source diff --git a/packages/server/src/server/daemon-e2e/muse.real.e2e.test.ts b/packages/server/src/server/daemon-e2e/muse.real.e2e.test.ts index d9d473511a..f8355065f4 100644 --- a/packages/server/src/server/daemon-e2e/muse.real.e2e.test.ts +++ b/packages/server/src/server/daemon-e2e/muse.real.e2e.test.ts @@ -10,7 +10,7 @@ import { BuiltinPluginLoader } from "../plugins/builtin/index.js"; import { createTestPaseoDaemon } from "../test-utils/paseo-daemon.js"; import { DaemonClient } from "../test-utils/daemon-client.js"; -test("Muse publishes its client settings bundle while installed plugins are disabled", async () => { +test("Muse registers without a client settings bundle", async () => { const daemon = await createTestPaseoDaemon({ builtinPlugins: new BuiltinPluginLoader(undefined, ["muse-provider"]), }); @@ -21,8 +21,8 @@ test("Muse publishes its client settings bundle while installed plugins are disa try { await client.connect(); const muse = (await client.getPluginCatalog()).find((plugin) => plugin.id === "muse-provider"); - expect(muse?.clientBundle).toBeTypeOf("string"); - expect(muse?.clientBundle).not.toBe(""); + expect(muse).toBeDefined(); + expect(muse?.clientBundle).toBe(""); } finally { await client.close(); await daemon.close(); diff --git a/plugins/muse-provider/client/settings.tsx b/plugins/muse-provider/client/settings.tsx deleted file mode 100644 index b057cf2cb0..0000000000 --- a/plugins/muse-provider/client/settings.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import { useCallback, useMemo } from "react"; -import { Text } from "react-native"; -import { useSettings, type PluginScreenProps, type SettingsState } from "@getpaseo/plugin/client"; -import { - SettingsAction, - SettingsCard, - SettingsSection, - SettingsSelect, - SettingsSwitch, -} from "@getpaseo/plugin/client/ui"; -import { museSettings } from "../shared/settings.js"; - -const networks = [ - { label: "Proxy only", value: "proxy-only" }, - { label: "Restricted", value: "restricted" }, - { label: "Enabled", value: "enabled" }, -] as const; -type ReadySettings = Extract, { status: "ready" }>; - -function Controls({ - settings, - theme, -}: { - settings: ReadySettings; - theme: PluginScreenProps["theme"]; -}) { - const style = useMemo(() => ({ color: theme.colors.foreground }), [theme]); - const changeSandbox = useCallback( - (sandbox: boolean) => { - void settings.save({ ...settings.values, sandbox }, settings.revision); - }, - [settings], - ); - const changeNetwork = useCallback( - (network: ReadySettings["values"]["network"]) => { - void settings.save({ ...settings.values, network }, settings.revision); - }, - [settings], - ); - const changeTrust = useCallback( - (trustWorkspace: boolean) => { - void settings.save({ ...settings.values, trustWorkspace }, settings.revision); - }, - [settings], - ); - return ( - - - - - - - - Changes apply to new Muse sessions. Disabling the sandbox enables full network access. Trust - workspace lets Muse load project skills and configuration. - - {settings.saveError ? ( - - {settings.saveError} - - ) : null} - - ); -} - -export function MuseSettings({ theme }: PluginScreenProps) { - const settings = useSettings(museSettings); - const style = useMemo(() => ({ color: theme.colors.foreground }), [theme]); - if (settings.status === "loading") return Loading settings…; - if (settings.status !== "ready") - return ( - - - {settings.error} - - - {settings.status === "invalid" ? ( - - ) : null} - - ); - return ; -} diff --git a/plugins/muse-provider/index.client.tsx b/plugins/muse-provider/index.client.tsx deleted file mode 100644 index 98db6de7a4..0000000000 --- a/plugins/muse-provider/index.client.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import type { PluginClientContext } from "@getpaseo/plugin/client"; -import { MuseSettings } from "./client/settings.js"; - -export default function contribute(client: PluginClientContext) { - client.addScreen({ - id: "muse", - title: "Muse Code", - Component: MuseSettings, - }); - client.addCommandCenterItem({ - id: "settings", - title: "Configure Muse Code", - icon: "Settings", - context: "global", - onSelect({ openScreen }) { - openScreen({ screenId: "muse" }); - }, - }); - return () => {}; -} diff --git a/plugins/muse-provider/index.server.ts b/plugins/muse-provider/index.server.ts index b5e988c23a..3ee054adce 100644 --- a/plugins/muse-provider/index.server.ts +++ b/plugins/muse-provider/index.server.ts @@ -1,18 +1,13 @@ import type { PluginServerContext } from "@getpaseo/plugin/server"; import { createMuseProvider } from "./server/provider.js"; -import { museSettings } from "./shared/settings.js"; import { Usage } from "./server/usage.js"; export default function contribute( - server: Pick< - PluginServerContext, - "registerProvider" | "registerSettings" | "registerUsageSource" - >, + server: Pick, ) { - const settings = server.registerSettings(museSettings); const usage = new Usage(); server.registerUsageSource(usage.registration()); - server.registerProvider(createMuseProvider(settings, usage)); + server.registerProvider(createMuseProvider(usage)); return () => {}; } diff --git a/plugins/muse-provider/server/options.ts b/plugins/muse-provider/server/options.ts new file mode 100644 index 0000000000..27621dace6 --- /dev/null +++ b/plugins/muse-provider/server/options.ts @@ -0,0 +1,32 @@ +import { z } from "zod"; +import { MuseError } from "./errors.js"; + +const optionsSchema = z + .object({ + sandbox: z + .object({ + enabled: z.boolean().default(false), + network: z.enum(["proxy-only", "restricted", "enabled"]).default("proxy-only"), + }) + .strict() + .prefault({}), + trustWorkspace: z.boolean().default(true), + }) + .strict(); + +export function serveArgs(providerOptions: unknown): string[] { + const parsed = optionsSchema.safeParse(providerOptions === undefined ? {} : providerOptions); + if (!parsed.success) { + const errors = parsed.error.issues + .map((issue) => `${["providerOptions", ...issue.path].join(".")}: ${issue.message}`) + .join("; "); + throw new MuseError("invalidProviderOptions", `Invalid Muse providerOptions: ${errors}`); + } + const { sandbox, trustWorkspace } = parsed.data; + return [ + "--sandbox-network", + sandbox.network, + ...(sandbox.enabled ? [] : ["--disable-sandbox"]), + ...(trustWorkspace ? ["--trust-workspace"] : []), + ]; +} diff --git a/plugins/muse-provider/server/provider.ts b/plugins/muse-provider/server/provider.ts index 10e000b0c1..bef6693f91 100644 --- a/plugins/muse-provider/server/provider.ts +++ b/plugins/muse-provider/server/provider.ts @@ -8,9 +8,7 @@ import { type ProviderRegistration, type ProviderStatus, } from "@getpaseo/plugin/server/provider"; -import type { PluginSettings } from "@getpaseo/plugin/server"; -import { serveArgs } from "./settings.js"; -import { settingsSchema } from "../shared/settings.js"; +import { serveArgs } from "./options.js"; import { Usage } from "./usage.js"; import { execFile } from "node:child_process"; import { Catalog, launchKey } from "./catalog.js"; @@ -31,10 +29,7 @@ const capabilities = [ "session.list", ] as const; -export function createMuseProvider( - settings: PluginSettings, - usage: Usage, -): ProviderRegistration { +export function createMuseProvider(usage: Usage): ProviderRegistration { return { id: "muse", label: "Muse Code", @@ -56,7 +51,6 @@ export function createMuseProvider( return connect( requireLaunch(request.launch), negotiateProviderCapabilities(request.capabilities, capabilities), - settings, usage, ); }, @@ -101,7 +95,6 @@ async function status(launch: ProviderLaunch): Promise { function connect( launch: ProviderLaunch, negotiated: readonly string[], - settings: PluginSettings, usage: Usage, ): ProviderConnection { const listeners = new Set<(event: ProviderEvent) => void>(); @@ -134,7 +127,7 @@ function connect( launch, emit, capabilities: negotiated, - serveArgs: await serveArgs(settings), + serveArgs: serveArgs(input.config.providerOptions), }); sessions.set(input.sessionId, session); try { diff --git a/plugins/muse-provider/server/settings.ts b/plugins/muse-provider/server/settings.ts deleted file mode 100644 index 21586834ce..0000000000 --- a/plugins/muse-provider/server/settings.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { PluginSettings } from "@getpaseo/plugin/server"; -import { settingsSchema } from "../shared/settings.js"; -import { MuseError } from "./errors.js"; - -export async function serveArgs( - settings: PluginSettings, -): Promise { - const state = await settings.read(); - if (state.status === "invalid") throw new MuseError("invalidSettings", state.error); - const { sandbox, network, trustWorkspace } = state.values; - return [ - "--sandbox-network", - network, - ...(sandbox ? [] : ["--disable-sandbox"]), - ...(trustWorkspace ? ["--trust-workspace"] : []), - ]; -} diff --git a/plugins/muse-provider/shared/settings.ts b/plugins/muse-provider/shared/settings.ts deleted file mode 100644 index f1af368e8c..0000000000 --- a/plugins/muse-provider/shared/settings.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { z } from "zod"; -import { defineSettings } from "@getpaseo/plugin"; - -export const settingsSchema = z.object({ - sandbox: z - .boolean() - .default(true) - .describe("Keep Muse filesystem and network sandboxing enabled"), - network: z - .enum(["proxy-only", "restricted", "enabled"]) - .default("proxy-only") - .describe("Sandbox network access; disabling sandbox forces full network access"), - trustWorkspace: z - .boolean() - .default(false) - .describe("Let Muse load project-scoped skills and config from the workspace"), -}); - -export const museSettings = defineSettings({ - id: "muse", - scope: "host", - version: 1, - schema: settingsSchema, -}); diff --git a/plugins/muse-provider/test/provider.test.ts b/plugins/muse-provider/test/provider.test.ts index 6c060fbfdb..1d346a0c29 100644 --- a/plugins/muse-provider/test/provider.test.ts +++ b/plugins/muse-provider/test/provider.test.ts @@ -13,7 +13,6 @@ import { type ProviderLaunch, type ProviderInput, } from "@getpaseo/plugin/server/provider"; -import { settingsSchema } from "../shared/settings.js"; import type { UsageSourceRegistration } from "@getpaseo/plugin/server"; import contribute from "../index.server.js"; @@ -25,21 +24,10 @@ afterEach(async () => { await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true }))); }); function registration( - readSettings = () => settingsSchema.parse({}), registerUsageSource = (_source: UsageSourceRegistration) => {}, ): ProviderRegistration { let provider: ProviderRegistration | undefined; const server = { - registerSettings() { - return { - read: async () => ({ - status: "ready" as const, - revision: "1", - values: readSettings(), - }), - subscribe: () => () => {}, - }; - }, registerUsageSource, registerProvider(value: ProviderRegistration) { provider = value; @@ -49,7 +37,11 @@ function registration( if (!provider) throw new Error("Provider was not registered"); return provider; } -async function harness(scenario = "text-reasoning", env: Record = {}) { +async function harness( + scenario = "text-reasoning", + env: Record = {}, + providerOptions?: Record, +) { const root = await mkdtemp(path.join(os.tmpdir(), "muse-provider-test-")); roots.push(root); const requests = path.join(root, "requests.ndjson"); @@ -59,14 +51,10 @@ async function harness(scenario = "text-reasoning", env: Record env: { MUSE_TEST_SCENARIO: scenario, MUSE_TEST_REQUESTS: requests, ...env }, }; await writeFile(requests, ""); - let settings = settingsSchema.parse({}); let usageSource!: UsageSourceRegistration; - const provider = registration( - () => settings, - (source) => { - usageSource = source; - }, - ); + const provider = registration((source) => { + usageSource = source; + }); const connection = await provider.connect({ launch, versions: [1], @@ -103,6 +91,7 @@ async function harness(scenario = "text-reasoning", env: Record sessionId: "paseo-session", config: { cwd: root, + providerOptions, env: {}, settings: {}, mcpServers: {}, @@ -155,9 +144,6 @@ async function harness(scenario = "text-reasoning", env: Record recorded, root, usageSource, - setSettings: (values: unknown) => { - settings = settingsSchema.parse(values); - }, }; } @@ -1011,22 +997,81 @@ test("silent active turns page after two minutes and stop polling on recovered t await vi.advanceTimersByTimeAsync(240000); expect((await h.recorded()).filter((f) => f.method === "view/page")).toHaveLength(1); }); -test("sandbox, network and workspace trust settings apply only on the next open", async () => { +test("default launch disables sandbox and trusts workspace", async () => { const h = await harness(); await h.open(); - h.setSettings({ sandbox: false, network: "enabled", trustWorkspace: true }); - expect((await h.recorded()).filter((f) => f.event === "hostStarted").map((f) => f.args)).toEqual([ - ["serve", "--sandbox-network", "proxy-only"], + expect((await h.recorded()).find((f) => f.event === "hostStarted").args).toEqual([ + "serve", + "--sandbox-network", + "proxy-only", + "--disable-sandbox", + "--trust-workspace", ]); - await h.send({ type: "session.close", sessionId: "paseo-session", requestId: "close-settings" }); +}); +test.each([ + [{ sandbox: { enabled: true } }, ["--sandbox-network", "proxy-only", "--trust-workspace"]], + [{ trustWorkspace: false }, ["--sandbox-network", "proxy-only", "--disable-sandbox"]], + [ + { sandbox: { network: "restricted" } }, + ["--sandbox-network", "restricted", "--disable-sandbox", "--trust-workspace"], + ], + [ + { sandbox: { network: "enabled" } }, + ["--sandbox-network", "enabled", "--disable-sandbox", "--trust-workspace"], + ], + [ + { sandbox: { enabled: true, network: "proxy-only" }, trustWorkspace: false }, + ["--sandbox-network", "proxy-only"], + ], +])("provider options %j control launch flags", async (params, args) => { + const h = await harness("text-reasoning", {}, params); + await h.open(); + expect((await h.recorded()).find((f) => f.event === "hostStarted").args).toEqual([ + "serve", + ...args, + ]); +}); +test.each([ + [{ sandbox: { enabled: "true" } }, "sandbox.enabled"], + [{ sandbox: { network: "invalid" } }, "sandbox.network"], + [{ trustWorkspace: 1 }, "trustWorkspace"], + [{ unexpected: true }, "unexpected"], +])("invalid provider options %j fail session creation clearly", async (options, field) => { + const h = await harness("text-reasoning", {}, options); + const event = await h.open(); + expect(event).toMatchObject({ + type: "request.failed", + error: { code: "invalidProviderOptions" }, + }); + if (event.type !== "request.failed") throw new Error("Expected failure"); + expect(event.error.message).toContain("Invalid Muse providerOptions"); + expect(event.error.message).toContain(field); + expect(await h.recorded()).toEqual([]); +}); +test("resuming a session reapplies its provider options to the new host", async () => { + const h = await harness( + "resume-without-cursor", + {}, + { + sandbox: { enabled: true, network: "restricted" }, + trustWorkspace: false, + }, + ); + await h.open({ version: 1, data: { sessionId: "saved-session" } }); + const saved = h.events.find((e) => e.type === "session.opened"); + if (saved?.type !== "session.opened" || !saved.persistence) + throw new Error("Expected persistence"); + await h.send({ type: "session.close", sessionId: "paseo-session", requestId: "close" }); await h.wait((e) => e.type === "session.closed"); const from = h.events.length; - await h.open(); + await h.open(saved.persistence); await h.wait((e) => e.type === "session.ready", from); - expect((await h.recorded()).filter((f) => f.event === "hostStarted").map((f) => f.args)).toEqual([ - ["serve", "--sandbox-network", "proxy-only"], - ["serve", "--sandbox-network", "enabled", "--disable-sandbox", "--trust-workspace"], + const frames = await h.recorded(); + expect(frames.filter((f) => f.event === "hostStarted").map((f) => f.args)).toEqual([ + ["serve", "--sandbox-network", "restricted"], + ["serve", "--sandbox-network", "restricted"], ]); + expect(frames.some((f) => f.method === "session/resume")).toBe(true); }); test("sessions list filters workspace and imported persistence opens via resume", async () => { const h = await harness("resume-without-cursor"); @@ -1203,8 +1248,7 @@ test("default workflow children render structured tool details from the real no- for (const source of ["user", "project"] as const) { test(`real ${source} skill catalog and command replay with explicit workspace trust`, async () => { - const h = await harness(`phase3-${source}-skill`); - h.setSettings({ trustWorkspace: source === "project" }); + const h = await harness(`phase3-${source}-skill`, {}, { trustWorkspace: source === "project" }); await h.open(); expect(await h.wait((e) => e.type === "session.commands")).toMatchObject({ commands: expect.arrayContaining([ diff --git a/public-docs/muse-code.md b/public-docs/muse-code.md index 99c3096755..83dea4d244 100644 --- a/public-docs/muse-code.md +++ b/public-docs/muse-code.md @@ -27,20 +27,47 @@ In Paseo, select **Muse Code**, then choose a model, approval mode, and reasonin Paseo runs `muse serve` and communicates over the Muse Session Protocol (MSP). Your Muse configuration determines the available models. -## Plugin settings +## Provider options -Open Command Center and select **Configure Muse Code**. Changes apply when an agent's -Muse host next opens, including a refresh of an existing agent. +Paseo launches Muse with the sandbox disabled (`--disable-sandbox`) and workspace trust +on (`--trust-workspace`). Muse can install packages using your npm cache and load project +rules, skills, and configuration. -| Setting | Default | Effect | -| --------------- | ------------ | ----------------------------------------------------------------------------------------------- | -| Sandbox | On | Keeps Muse's filesystem and network sandbox enabled. Turning it off allows full network access. | -| Network | `proxy-only` | Muse's sandbox network policy: `proxy-only`, `restricted`, or `enabled`. | -| Trust workspace | Off | Lets Muse load project-scoped skills and configuration. | +Set defaults for every Muse agent in `config.json` on the daemon machine under +`agents.providers.muse.options`: + +```json +{ + "agents": { + "providers": { + "muse": { + "options": { + "sandbox": { "enabled": true, "network": "proxy-only" }, + "trustWorkspace": false + } + } + } + } +} +``` + +You can also supply per-agent `providerOptions` when creating an agent. The daemon merges +provider defaults with the per-agent options and delivers the effective `providerOptions` +to Muse. See [provider options](/docs/sdk/provider-options) for configuration and creation. + +Per-agent options are saved with the agent and reapplied when its Muse host opens on refresh +or resume. Invalid values and unknown keys fail session creation with a `providerOptions` +error. + +| Option | Default | Effect | +| ----------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------- | +| `sandbox.enabled` | `false` | Enables Muse's filesystem and network sandbox. Its read-only home directory prevents npm from writing the default cache. | +| `sandbox.network` | `"proxy-only"` | Sandbox network policy: `"proxy-only"`, `"restricted"`, or `"enabled"`. With sandbox disabled, Muse allows full network access. | +| `trustWorkspace` | `true` | Lets Muse load project rules, skills, and configuration. | Approval modes control tool decisions separately: **Default**, **Ask**, **Strict**, and -**Full access**. Full access automatically allows escalated approval stages; it keeps the -sandbox setting you chose. Skills appear in the slash-command menu, alongside `/compact`. +**Full access**. Full access automatically allows escalated approval stages; it preserves +the effective provider options. Skills appear in the slash-command menu, alongside `/compact`. ## Limitations in Muse 1.4.1 From 1f5f5d384e9409240c11fc4491fcdb247ea4bc14 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Wed, 30 Sep 2026 21:09:07 +0100 Subject: [PATCH 032/100] Sidebar usage shows default percents with logos, and remembers the picked host (#5786) * fix(app): show default usage windows when no windows are pinned * fix(app): show only percentages in sidebar usage summaries * fix(client): restore usage source logos for released hosts * fix(app): restore the labeled footer line below sidebar items * fix(app): use History's host filter to pick the usage and plugin host The Usage screen, the compact usage sheet and plugin screens used a separate HostSwitcher trigger. They now use HostFilter without "All hosts", as the label manager does, and hide it with one host, as History does. * fix(app): save the usage host pick and show it in the sidebar summary Picking a host on the Usage screen or usage sheet was view state: it reset when the view closed, and the sidebar summary never read it. The pick is now a device-local usage preference. The summary and the screen show the picked host while it is connected, else the active workspace's host, else the first host. The usage fixture answers by request, since the summary and the screen each load reports and only a card's Refresh forces one. * test(app): expect plugin footer rows above the footer bottom line Add project moved to the bottom line with the icons, so footer rows sit above it, spanning Add project to Settings. * test(app): expect the saved usage host in stored settings * test(app): let usage settings specs share reports with the sidebar summary The sidebar summary loads reports on app start, so the settings specs answer by request instead of by order and read percents from the settings card. --- .../e2e/browser/plugin-sidebar-items.spec.ts | 13 +- .../e2e/browser/sidebar-nav-settings.spec.ts | 57 ++++++++ packages/app/e2e/browser/usage-screen.spec.ts | 114 ++++++++++++---- .../app/e2e/browser/usage-settings.spec.ts | 31 +++-- .../e2e/browser/usage-sidebar-item.spec.ts | 83 ++++++++---- .../e2e/support/helpers/add-project-flow.ts | 2 +- .../support/helpers/sidebar-nav-settings.ts | 45 ++++++- .../app/e2e/support/helpers/usage-reports.ts | 78 ++++++++--- .../app/src/components/hosts/host-filter.tsx | 10 +- .../app/src/components/hosts/host-picker.tsx | 2 +- .../src/components/hosts/host-switcher.tsx | 83 ------------ packages/app/src/components/left-sidebar.tsx | 126 ++++++++++-------- .../components/sidebar/sidebar-separator.tsx | 4 +- .../src/hooks/use-settings/storage.test.ts | 10 +- packages/app/src/plugins/surface-screen.tsx | 17 ++- .../settings/sidebar/sidebar-nav-section.tsx | 2 +- packages/app/src/sidebar-nav/model.test.ts | 1 + packages/app/src/sidebar-nav/model.ts | 4 +- packages/app/src/usage/controls.tsx | 26 ++-- packages/app/src/usage/copy.ts | 1 - packages/app/src/usage/hosts.ts | 48 +++++-- packages/app/src/usage/model.test.ts | 51 ++++--- packages/app/src/usage/model.ts | 46 +++---- packages/app/src/usage/pinned.test.ts | 61 ++++++++- packages/app/src/usage/pinned.ts | 58 ++++---- packages/app/src/usage/preferences.test.ts | 22 ++- packages/app/src/usage/preferences.ts | 13 +- packages/app/src/usage/sidebar-item.tsx | 11 +- packages/client/src/daemon-client.test.ts | 40 ++++++ packages/client/src/daemon-client.ts | 2 + packages/client/src/legacy-usage-icons.ts | 21 +++ 31 files changed, 717 insertions(+), 365 deletions(-) delete mode 100644 packages/app/src/components/hosts/host-switcher.tsx create mode 100644 packages/client/src/legacy-usage-icons.ts diff --git a/packages/app/e2e/browser/plugin-sidebar-items.spec.ts b/packages/app/e2e/browser/plugin-sidebar-items.spec.ts index 24b90bfe4e..b36ea6ba98 100644 --- a/packages/app/e2e/browser/plugin-sidebar-items.spec.ts +++ b/packages/app/e2e/browser/plugin-sidebar-items.spec.ts @@ -40,22 +40,21 @@ async function qaScreenshot(page: Page, name: string, area?: Locator) { else await page.screenshot({ path: file }); } -/** Footer rows sit between the fixed Add project row and the fixed icon row, full width. */ +/** Footer rows sit above the fixed bottom line, spanning it from Add project to Settings. */ async function expectFooterRow(page: Page, row: Locator) { // Polled: on compact the drawer is still sliding in when the row first shows. await expect .poll(async () => { const addProject = (await visibleTestId(page, "sidebar-add-project").boundingBox())!; - const hosts = (await visibleTestId(page, "sidebar-hosts-trigger").boundingBox())!; + const settings = (await visibleTestId(page, "sidebar-settings").boundingBox())!; const box = (await row.boundingBox())!; return { - belowAddProject: box.y >= addProject.y + addProject.height, - aboveIconRow: box.y + box.height <= hosts.y, + aboveBottomLine: box.y + box.height <= addProject.y, alignedLeft: Math.abs(box.x - addProject.x) < 1, - fullWidth: Math.abs(box.width - addProject.width) < 1, + alignedRight: Math.abs(box.x + box.width - (settings.x + settings.width)) < 1, }; }) - .toEqual({ belowAddProject: true, aboveIconRow: true, alignedLeft: true, fullWidth: true }); + .toEqual({ aboveBottomLine: true, alignedLeft: true, alignedRight: true }); } function botRow(page: Page, rowId: string): Locator { @@ -188,7 +187,7 @@ test.describe("Plugin sidebar items", () => { await expect(page.getByText("Deploys screen body", { exact: true })).toHaveCount(0); }); - await test.step("the footer row renders as a row between Add project and the icon row", async () => { + await test.step("the footer row renders as a row above the bottom line", async () => { await expect(sync).toHaveAccessibleName("Sync"); await expectFooterRow(page, sync); await expect(visibleTestId(page, "sidebar-usage")).toBeVisible(); diff --git a/packages/app/e2e/browser/sidebar-nav-settings.spec.ts b/packages/app/e2e/browser/sidebar-nav-settings.spec.ts index 29a6807c99..038ea63f95 100644 --- a/packages/app/e2e/browser/sidebar-nav-settings.spec.ts +++ b/packages/app/e2e/browser/sidebar-nav-settings.spec.ts @@ -1,8 +1,18 @@ import { expect, test } from "../support/fixtures"; +import { openAddProjectFlow, addProjectFlow } from "../support/helpers/add-project-flow"; +import { installUsageReportsFixture } from "../support/helpers/usage-reports"; +import { + claudeAndCodexReports, + expectPinnedUsage, + openCompactSidebar, +} from "../support/helpers/usage-sidebar-item"; import { gotoAppShell } from "../support/helpers/app"; import { SHOWCASE_PLUGIN_ID, installSidebarPlugins } from "../support/helpers/plugin-sidebar-items"; import { expectFooterIconRow, + expectFooterSeparator, + hoverFooterAddProject, + footerScreenshot, expectFooterItemHidden, expectFooterOrder, expectFooterSettingsKeys, @@ -21,6 +31,50 @@ import { setSidebarNavItemVisible, } from "../support/helpers/sidebar-nav-settings"; +test("fixed footer line keeps the labeled Add project button and four icons", async ({ page }) => { + test.setTimeout(120_000); + await installUsageReportsFixture(page, { lists: [() => claudeAndCodexReports()] }); + await seedSidebarFooterPreferences(page, [{ key: "add-project", visible: false }]); + await page.setViewportSize({ width: 1440, height: 900 }); + await gotoAppShell(page); + await expectPinnedUsage(page, ["31%", "7%"]); + await expectFooterIconRow(page); + await expectFooterSeparator(page, true); + await footerScreenshot(page, "footer-desktop-with-rows"); + await hoverFooterAddProject(page); + await footerScreenshot(page, "footer-desktop-add-project-tooltip"); + await openAddProjectFlow(page); + await page.keyboard.press("Escape"); + await expect(addProjectFlow(page)).toBeHidden(); + await openSidebarNavSettings(page); + await expectFooterSettingsKeys(page, ["usage"]); + await setFooterItemVisible(page, "usage", false); + await leaveSettings(page); + await expectFooterSeparator(page, false); + await expectFooterIconRow(page); + await footerScreenshot(page, "footer-desktop-without-rows"); + await page.setViewportSize({ width: 390, height: 844 }); + await openCompactSidebar(page); + await expectFooterSeparator(page, false); + await expectFooterIconRow(page); + await footerScreenshot(page, "footer-compact-without-rows"); + await page.locator('[data-testid="sidebar-add-project"]:visible').hover(); + await expect(page.getByTestId("sidebar-add-project-tooltip")).toHaveCount(0); + await footerScreenshot(page, "footer-compact-add-project-hover"); + await openAddProjectFlow(page); + await page.keyboard.press("Escape"); + await expect(addProjectFlow(page)).toBeHidden(); + await page.setViewportSize({ width: 1440, height: 900 }); + await openSidebarNavSettings(page); + await setFooterItemVisible(page, "usage", true); + await leaveSettings(page); + await page.setViewportSize({ width: 390, height: 844 }); + await openCompactSidebar(page); + await expectFooterSeparator(page, true); + await expectFooterIconRow(page); + await footerScreenshot(page, "footer-compact-with-rows"); +}); + test.describe("Sidebar items in Appearance settings", () => { test("owner reorders and hides top-level sidebar items", async ({ page }) => { await gotoAppShell(page); @@ -147,6 +201,7 @@ test.describe("Sidebar footer rows in Appearance settings", () => { await leaveSettings(page); await expectFooterOrder(page, ["usage", syncKey]); await expectFooterIconRow(page); + await expectFooterSeparator(page, true); }); await test.step("moving Sync up and hiding the Usage item changes the rows", async () => { @@ -160,6 +215,7 @@ test.describe("Sidebar footer rows in Appearance settings", () => { page.locator(`[data-testid="plugin-sidebar-footer-${SHOWCASE_PLUGIN_ID}-sync"]:visible`), ).toBeVisible(); await expectFooterIconRow(page); + await expectFooterSeparator(page, true); }); await test.step("the rows keep that shape across a reload", async () => { @@ -169,6 +225,7 @@ test.describe("Sidebar footer rows in Appearance settings", () => { ).toBeVisible({ timeout: 30_000 }); await expectFooterItemHidden(page, "usage"); await expectFooterIconRow(page); + await expectFooterSeparator(page, true); await openSidebarNavSettings(page); await expectFooterSettingsKeys(page, [syncKey, "usage", brokenKey]); }); diff --git a/packages/app/e2e/browser/usage-screen.spec.ts b/packages/app/e2e/browser/usage-screen.spec.ts index 37e0ae26aa..78deb7835f 100644 --- a/packages/app/e2e/browser/usage-screen.spec.ts +++ b/packages/app/e2e/browser/usage-screen.spec.ts @@ -1,10 +1,22 @@ +import { randomUUID } from "node:crypto"; import path from "node:path"; import type { Page } from "@playwright/test"; import type { UsageReportEntry } from "@getpaseo/protocol/messages"; import { expect, test } from "../support/fixtures"; import { gotoAppShell } from "../support/helpers/app"; +import { addConnectedHostAndReload } from "../support/helpers/hosts"; +import { startIsolatedHostDaemon } from "../support/helpers/isolated-host-daemon"; import { getServerId } from "../support/helpers/server-id"; -import { installUsageReportsFixture } from "../support/helpers/usage-reports"; +import { + installUsageReportsFixture, + type UsageListRequest, + type UsageReportsFixture, +} from "../support/helpers/usage-reports"; +import { expectPinnedUsage, usageItem } from "../support/helpers/usage-sidebar-item"; + +function forcedRefreshes(usage: UsageReportsFixture): UsageListRequest[] { + return usage.listRequests().filter((request) => request.forceRefresh); +} // Two hours reads "2h ago" for an hour, so the assertion cannot race the clock. function twoHoursAgo(): string { @@ -20,15 +32,25 @@ async function qaScreenshot(page: Page, name: string) { await page.screenshot({ path: path.join(directory, `${name}.png`) }); } -/** The host selector names the host shown, even when it is the only one. */ -async function shownHostLabel(page: Page): Promise { - const selector = page.locator('[data-testid="usage-host-switcher"]:visible'); - await expect(selector).toHaveAccessibleName(/^Usage host: .+/, { timeout: 10_000 }); - return (await selector.innerText()).trim(); +function hostFilter(page: Page) { + return page.locator('[data-testid="usage-host-filter-trigger"]:visible'); +} + +function weeklyReport(sourceId: string, usedPct: number): UsageReportEntry { + return { + id: `${sourceId}:a`, + account: {}, + fetchedAt: twoHoursAgo(), + sourceId, + sourceLabel: `${sourceId} plan`, + report: { status: "available", windows: [{ id: "weekly", label: "Weekly", usedPct }] }, + }; } test.describe("usage screen", () => { - test("opens from the sidebar on the host's reports, naming the one host", async ({ page }) => { + test("opens from the sidebar on the host's reports, with no host filter for one host", async ({ + page, + }) => { test.setTimeout(120_000); const serverId = getServerId(); const usage = await installUsageReportsFixture(page, { @@ -64,10 +86,7 @@ test.describe("usage screen", () => { const group = page.getByTestId(`usage-host-${serverId}`); await expect(group.getByText("Alpha plan", { exact: true })).toBeVisible({ timeout: 10_000 }); - const hostLabel = await shownHostLabel(page); - await expect(page.locator('[data-testid="usage-host-switcher"]:visible')).toHaveAccessibleName( - `Usage host: ${hostLabel}`, - ); + await expect(hostFilter(page)).toHaveCount(0); await qaScreenshot(page, "phase7-usage-screen-one-host"); await expect(group.getByText("31%")).toBeVisible(); await expect(group.getByText("Beta plan", { exact: true })).toBeVisible(); @@ -103,13 +122,13 @@ test.describe("usage screen", () => { windows: [{ id: "weekly", label: "Weekly", usedPct }], }, }); + // The sidebar summary and the screen each load reports; only a card's Refresh forces one. const usage = await installUsageReportsFixture(page, { lists: [ - [alpha(31, twoHoursAgo()), beta], - () => [ - alpha(58, new Date().toISOString()), - { ...beta, fetchedAt: new Date().toISOString() }, - ], + (request) => + request.forceRefresh + ? [alpha(58, new Date().toISOString())] + : [alpha(31, twoHoursAgo()), beta], ], }); @@ -120,11 +139,9 @@ test.describe("usage screen", () => { const alphaRefresh = group.getByTestId("usage-refresh").first(); await alphaRefresh.click(); - await usage.waitForListRequests(2); - expect(usage.listRequests()).toEqual([ - { forceRefresh: false, reportIds: undefined }, - { forceRefresh: true, reportIds: ["alpha:a"] }, - ]); + await expect + .poll(() => forcedRefreshes(usage)) + .toEqual([{ forceRefresh: true, reportIds: ["alpha:a"] }]); await expect(group.getByText("58%")).toBeVisible(); await expect(group.getByText("12%")).toBeVisible(); @@ -169,13 +186,60 @@ test.describe("usage screen", () => { await gotoAppShell(page); await page.locator('[data-testid="sidebar-usage"]:visible').first().click(); - const hostLabel = await shownHostLabel(page); await expect( - page.getByTestId(`usage-host-${serverId}`).getByText(`Update ${hostLabel} to see usage`, { - exact: true, - }), + page.getByTestId(`usage-host-${serverId}`).getByText(/^Update .+ to see usage$/), ).toBeVisible({ timeout: 10_000 }); await qaScreenshot(page, "phase7-usage-update-host"); expect(usage.listRequests()).toHaveLength(0); }); + + test("a host picked on the Usage screen is the sidebar's host too, after a reload", async ({ + page, + }) => { + test.setTimeout(420_000); + const primaryServerId = getServerId(); + const secondary = await startIsolatedHostDaemon( + `srv_usage_${randomUUID().replaceAll("-", "").slice(0, 12)}`, + ); + try { + await installUsageReportsFixture(page, { lists: [[weeklyReport("alpha", 31)]] }); + await installUsageReportsFixture(page, { + port: secondary.port, + lists: [[weeklyReport("beta", 12)]], + }); + await gotoAppShell(page); + await addConnectedHostAndReload(page, { + serverId: secondary.serverId, + label: "Secondary box", + port: secondary.port, + }); + + // Nothing picked and no workspace open: the first host. + await expectPinnedUsage(page, ["31%"]); + await usageItem(page).click(); + await expect(page.getByTestId(`usage-host-${primaryServerId}`)).toBeVisible(); + + await hostFilter(page).click(); + await page.getByTestId(`usage-host-filter-item-${secondary.serverId}`).click(); + await expect( + page.getByTestId(`usage-host-${secondary.serverId}`).getByText("12%"), + ).toBeVisible({ timeout: 30_000 }); + await expect(hostFilter(page)).toContainText("Secondary box"); + await qaScreenshot(page, "usage-screen-picked-host"); + await expectPinnedUsage(page, ["12%"]); + + // The e2e seed resets the host list on every load, so reopening re-adds the second host. + await addConnectedHostAndReload(page, { + serverId: secondary.serverId, + label: "Secondary box", + port: secondary.port, + }); + await expectPinnedUsage(page, ["12%"]); + await expect( + page.getByTestId(`usage-host-${secondary.serverId}`).getByText("12%"), + ).toBeVisible({ timeout: 30_000 }); + } finally { + await secondary.close().catch(() => undefined); + } + }); }); diff --git a/packages/app/e2e/browser/usage-settings.spec.ts b/packages/app/e2e/browser/usage-settings.spec.ts index d99b93fdd2..1c590c38be 100644 --- a/packages/app/e2e/browser/usage-settings.spec.ts +++ b/packages/app/e2e/browser/usage-settings.spec.ts @@ -3,10 +3,17 @@ import { expect, test } from "../support/fixtures"; import { gotoAppShell, openSettings } from "../support/helpers/app"; import { getServerId } from "../support/helpers/server-id"; import { openSettingsHostSection } from "../support/helpers/settings"; -import { installUsageReportsFixture } from "../support/helpers/usage-reports"; +import { + installUsageReportsFixture, + type UsageReportsFixture, +} from "../support/helpers/usage-reports"; const ICON = ''; +function forcedRefreshCount(usage: UsageReportsFixture): number { + return usage.listRequests().filter((request) => request.forceRefresh).length; +} + function report(input: { sourceId: string; sourceLabel: string; @@ -69,7 +76,6 @@ test.describe("usage settings", () => { await gotoAppShell(page); await openSettings(page); - expect(usage.listRequests()).toHaveLength(0); await openSettingsHostSection(page, serverId, "usage"); await usage.waitForListRequests(1); @@ -92,20 +98,14 @@ test.describe("usage settings", () => { test.setTimeout(120_000); const serverId = getServerId(); const windows = (usedPct: number) => [{ id: "w", label: "Weekly", usedPct }]; + // The sidebar summary and the section each load reports; only Refresh forces one. const usage = await installUsageReportsFixture(page, { lists: [ - [ + (request) => [ report({ sourceId: "alpha", sourceLabel: "Alpha plan", - report: { windows: windows(23) }, - }), - ], - [ - report({ - sourceId: "alpha", - sourceLabel: "Alpha plan", - report: { windows: windows(64) }, + report: { windows: windows(request.forceRefresh ? 64 : 23) }, }), ], ], @@ -114,13 +114,12 @@ test.describe("usage settings", () => { await gotoAppShell(page); await openSettings(page); await openSettingsHostSection(page, serverId, "usage"); - await expect(page.getByText("23%")).toBeVisible({ timeout: 10_000 }); + const card = page.getByTestId("usage-card"); + await expect(card.getByText("23%")).toBeVisible({ timeout: 10_000 }); await page.getByRole("button", { name: "Refresh", exact: true }).click(); - await usage.waitForListRequests(2); - - expect(usage.listRequests().at(-1)).toEqual({ forceRefresh: true }); - await expect(page.getByText("64%")).toBeVisible(); + await expect.poll(() => forcedRefreshCount(usage)).toBe(1); + await expect(card.getByText("64%")).toBeVisible(); }); test("asks to update a host without usage support and never calls it", async ({ page }) => { diff --git a/packages/app/e2e/browser/usage-sidebar-item.spec.ts b/packages/app/e2e/browser/usage-sidebar-item.spec.ts index 4fdd06d62b..e09e4d22c6 100644 --- a/packages/app/e2e/browser/usage-sidebar-item.spec.ts +++ b/packages/app/e2e/browser/usage-sidebar-item.spec.ts @@ -8,7 +8,6 @@ import { leaveSettings, openSidebarNavSettings } from "../support/helpers/sideba import { installUsageReportsFixture } from "../support/helpers/usage-reports"; import { claudeAndCodexReports, - expectNoPinnedUsage, expectOnUsageScreen, expectPinnedUsage, leaveUsageScreen, @@ -33,7 +32,7 @@ async function qaScreenshot(page: Page, name: string, area: ScreenshotArea = { k await page.waitForTimeout(600); // Expo's fast-refresh indicator sits over the footer's Hosts icon. await page.addStyleTag({ content: ".__expo_fast_refresh { display: none !important; }" }); - const file = path.join(directory, `phase4-${name}.png`); + const file = path.join(directory, `${name}.png`); if (area.kind === "element") { await area.locator.screenshot({ path: file }); return; @@ -42,14 +41,9 @@ async function qaScreenshot(page: Page, name: string, area: ScreenshotArea = { k await page.screenshot({ path: file, clip }); } -/** The sidebar footer, from Add project down to the icon row, with some margin. */ +/** Includes summary rows above the fixed icon row. */ async function footerClip(page: Page) { - const top = (await page.locator('[data-testid="sidebar-add-project"]:visible').boundingBox())!; - const bottom = (await page.locator('[data-testid="sidebar-settings"]:visible').boundingBox())!; - const margin = 16; - const x = Math.max(0, top.x - margin); - const y = Math.max(0, top.y - margin); - return { x, y, width: 300, height: bottom.y + bottom.height + margin - y }; + return (await page.locator('[data-testid="sidebar-footer"]:visible').boundingBox())!; } test.describe("Usage item", () => { @@ -63,21 +57,32 @@ test.describe("Usage item", () => { await gotoAppShell(page); const screen = page.getByTestId(`usage-host-${serverId}`); - await test.step("a fresh device shows the plain Usage row, which opens the Usage screen", async () => { + await test.step("a fresh device shows default windows, which opens the Usage screen", async () => { await expect(usageItem(page)).toBeVisible({ timeout: 30_000 }); - await expectNoPinnedUsage(page); - await qaScreenshot(page, "desktop-footer-empty", { kind: "footer" }); + await expectPinnedUsage(page, ["31%", "7%"]); + await qaScreenshot(page, "desktop-footer-defaults", { kind: "footer" }); + await openSidebarNavSettings(page); + await qaScreenshot(page, "desktop-settings-sidebar-footer"); + await page.setViewportSize(COMPACT); + await qaScreenshot(page, "compact-settings-sidebar-footer"); + await gotoAppShell(page); + await openCompactSidebar(page); + await expectPinnedUsage(page, ["31%", "7%"]); + await qaScreenshot(page, "compact-footer-defaults"); + await page.setViewportSize(WIDE); + await gotoAppShell(page); await usageItem(page).click(); await expectOnUsageScreen(page); }); await test.step("pinning Claude 5-hour and Codex weekly shows both in the Usage item", async () => { await expect(screen.getByText("Claude", { exact: true })).toBeVisible({ timeout: 10_000 }); - // One host: the selector still names it. - await expect( - page.locator('[data-testid="usage-host-switcher"]:visible'), - ).toHaveAccessibleName(/^Usage host: .+/); + // One host: no host filter, as on History. + await expect(page.locator('[data-testid="usage-host-filter-trigger"]:visible')).toHaveCount( + 0, + ); await togglePin(screen, "Claude", "Session"); + await expectPinnedUsage(page, ["31%"]); await togglePin(screen, "Codex", "Weekly"); await expectPinnedUsage(page, ["31%", "12%"]); await expect(usageItem(page)).not.toHaveText("Usage"); @@ -95,7 +100,8 @@ test.describe("Usage item", () => { await test.step("remaining flips the Usage item and the Usage screen", async () => { await showUsageAs(page, "remaining"); - await expectPinnedUsage(page, ["69% left", "88% left"]); + await expectPinnedUsage(page, ["69%", "88%"]); + await expect(usageItem(page)).toHaveAccessibleName(/Claude .*69% left, Codex .*88% left/); await expect( screen.getByTestId("usage-report-claude:default").getByText("69% left"), ).toBeVisible(); @@ -117,7 +123,7 @@ test.describe("Usage item", () => { await leaveUsageScreen(page); await openCompactSidebar(page); await expect(usageItem(page)).toBeInViewport(); - await expectPinnedUsage(page, ["69% left", "88% left"]); + await expectPinnedUsage(page, ["69%", "88%"]); await qaScreenshot(page, "compact-footer"); await usageItem(page).click(); const sheet = usageSheet(page); @@ -129,10 +135,7 @@ test.describe("Usage item", () => { await expect(pinRow(sheet, "Claude", "Session")).toHaveAccessibleName( /^Pin Claude Session, \d+% left( · .+)?$/, ); - // The sheet carries the Usage screen's controls, host selector included. - await expect( - page.locator('[data-testid="usage-host-switcher"]:visible'), - ).toHaveAccessibleName(/^Usage host: .+/); + // The sheet carries the Usage screen's controls. await expect(page.locator('[data-testid="usage-refresh-all"]:visible')).toBeVisible(); await expect(page).not.toHaveURL(/\/usage$/); const sheetBox = (await sheet.boundingBox())!; @@ -150,7 +153,7 @@ test.describe("Usage item", () => { await test.step("a reload keeps the pins and the toggle", async () => { await page.reload(); - await expectPinnedUsage(page, ["69% left", "88% left"]); + await expectPinnedUsage(page, ["69%", "88%"]); await expect( page.locator('[data-testid="usage-display-remaining"]:visible').first(), ).toHaveAttribute("aria-selected", "true"); @@ -167,13 +170,13 @@ test.describe("Usage item", () => { await expect(screen.getByText("Claude", { exact: true })).toBeVisible({ timeout: 10_000 }); }); - await test.step("unpinning both brings back the Usage row, and that survives a reload", async () => { + await test.step("unpinning both brings back default windows, and that survives a reload", async () => { await togglePin(screen, "Claude", "Session"); await togglePin(screen, "Codex", "Weekly"); - await expectNoPinnedUsage(page); + await expectPinnedUsage(page, ["69%", "93%"]); await page.reload(); await expect(screen.getByText("88% left")).toBeVisible({ timeout: 10_000 }); - await expectNoPinnedUsage(page); + await expectPinnedUsage(page, ["69%", "93%"]); await gotoAppShell(page); await usageItem(page).click(); await expectOnUsageScreen(page); @@ -190,3 +193,31 @@ test.describe("Usage item", () => { }); }); }); + +test("released hosts supply source logos through the client conversion", async ({ page }) => { + await installUsageReportsFixture(page, { + lists: [() => claudeAndCodexReports()], + providerUsageListOnly: true, + }); + await page.setViewportSize(WIDE); + await gotoAppShell(page); + await expectPinnedUsage(page, ["31%", "7%"]); + // Source logos are decorative SVGs with no accessible role. Their path data distinguishes + // the source artwork from the fallback gauge. + const claudePath = /]* d="([^"]+)"/.exec(claudeAndCodexReports()[0]!.icon!)![1]!; + const codexPath = /]* d="([^"]+)"/.exec(claudeAndCodexReports()[1]!.icon!)![1]!; + const summary = usageItem(page).getByTestId("sidebar-usage-pinned-window"); + await expect(summary.nth(0).locator("svg path").first()).toHaveAttribute("d", claudePath); + await expect(summary.nth(1).locator("svg path").first()).toHaveAttribute("d", codexPath); + await qaScreenshot(page, "released-host-footer", { kind: "footer" }); + await usageItem(page).click(); + await expectOnUsageScreen(page); + const screen = page.getByTestId(`usage-host-${getServerId()}`); + await expect( + screen.getByTestId("usage-report-claude").locator("svg path").first(), + ).toHaveAttribute("d", claudePath); + await expect( + screen.getByTestId("usage-report-codex").locator("svg path").first(), + ).toHaveAttribute("d", codexPath); + await qaScreenshot(page, "released-host-usage-screen"); +}); diff --git a/packages/app/e2e/support/helpers/add-project-flow.ts b/packages/app/e2e/support/helpers/add-project-flow.ts index c7b15df456..bb88264d37 100644 --- a/packages/app/e2e/support/helpers/add-project-flow.ts +++ b/packages/app/e2e/support/helpers/add-project-flow.ts @@ -47,7 +47,7 @@ async function openAddProjectFlowSurface( page: Page, expectedPage: "host" | "method", ): Promise { - await page.getByTestId("sidebar-add-project").click(); + await page.locator('[data-testid="sidebar-add-project"]:visible').click(); await expect(addProjectFlow(page)).toBeVisible({ timeout: 30_000 }); await expectAddProjectPage(page, expectedPage); } diff --git a/packages/app/e2e/support/helpers/sidebar-nav-settings.ts b/packages/app/e2e/support/helpers/sidebar-nav-settings.ts index f57ee32497..b17b7b6bfb 100644 --- a/packages/app/e2e/support/helpers/sidebar-nav-settings.ts +++ b/packages/app/e2e/support/helpers/sidebar-nav-settings.ts @@ -1,3 +1,4 @@ +import path from "node:path"; import { expect, type Locator, type Page } from "@playwright/test"; import { openSettings } from "./app"; import { clickSettingsBackToWorkspace, openSettingsSection } from "./settings"; @@ -218,13 +219,14 @@ export async function expectFooterItemHidden(page: Page, key: string): Promise { const boxes = await Promise.all( FOOTER_ICON_TEST_IDS.map((testID) => @@ -232,10 +234,49 @@ export async function expectFooterIconRow(page: Page): Promise { ), ); const [first, ...rest] = boxes.map((box) => box!); + expect(first.width).toBeGreaterThan(rest[0]!.width); + await expect( + page + .locator('[data-testid="sidebar-add-project"]:visible') + .getByText("Add project", { exact: true }), + ).toBeVisible(); let previous = first; for (const box of rest) { - expect(Math.abs(box.y - first.y)).toBeLessThan(2); + expect(Math.abs(box.y + box.height / 2 - first.y - first.height / 2)).toBeLessThan(2); expect(box.x).toBeGreaterThan(previous.x); previous = box; } } + +export async function expectFooterSeparator(page: Page, shown: boolean): Promise { + await expect(page.locator('[data-testid="sidebar-footer"]:visible')).toHaveCSS( + "border-top-width", + "1px", + ); + await expect(page.locator('[data-testid="sidebar-footer-bottom-line"]:visible')).toHaveCSS( + "border-top-width", + "0px", + ); + const separator = page.locator('[data-testid="sidebar-footer-separator"]:visible'); + await expect(separator).toHaveCount(shown ? 1 : 0); + expect( + await separator.evaluateAll((elements) => + elements.map((element) => getComputedStyle(element).borderBottomWidth), + ), + ).toEqual(shown ? ["1px"] : []); +} + +export async function hoverFooterAddProject(page: Page): Promise { + await page.locator('[data-testid="sidebar-add-project"]:visible').hover(); + const tooltip = page.getByTestId("sidebar-add-project-tooltip"); + await expect(tooltip.getByText("Add project", { exact: true })).toBeVisible(); + await expect(tooltip.getByText("Ctrl+O", { exact: true })).toBeVisible(); +} + +export async function footerScreenshot(page: Page, name: string): Promise { + const directory = process.env.PASEO_QA_SCREENSHOT_DIR; + if (!directory) return; + await page.waitForTimeout(600); + await page.addStyleTag({ content: ".__expo_fast_refresh { display: none !important; }" }); + await page.screenshot({ path: path.join(directory, `${name}.png`) }); +} diff --git a/packages/app/e2e/support/helpers/usage-reports.ts b/packages/app/e2e/support/helpers/usage-reports.ts index 339475b8e6..4c853045fc 100644 --- a/packages/app/e2e/support/helpers/usage-reports.ts +++ b/packages/app/e2e/support/helpers/usage-reports.ts @@ -1,20 +1,30 @@ import type { Page } from "@playwright/test"; import type { UsageReportEntry } from "@getpaseo/protocol/messages"; -import { daemonWsRoutePattern } from "./daemon-port"; +import { daemonWsRoutePattern, wsRoutePatternForPort } from "./daemon-port"; + +export interface UsageListRequest { + forceRefresh: boolean; + reportIds?: string[]; +} export interface UsageReportsFixture { - listRequests(): Array<{ forceRefresh: boolean; reportIds?: string[] }>; + listRequests(): UsageListRequest[]; waitForListRequests(count: number): Promise; } interface UsageReportsFixtureOptions { /** * Successive `usage.list_reports` responses; the last one repeats. `{ error }` fails that - * request; a function builds the response when the request arrives (e.g. a fresh `fetchedAt`). + * request; a function builds the response from the request when it arrives (e.g. a fresh + * `fetchedAt`, or a different answer to a forced refresh). */ - lists?: Array UsageListResponse)>; + lists?: Array UsageListResponse)>; /** False simulates a host with no usage reporting capability. */ usageSupported?: boolean; + /** Released hosts expose provider.usage.list with no source icons. */ + providerUsageListOnly?: boolean; + /** The host daemon's port; defaults to the E2E daemon. */ + port?: number; } type UsageListResponse = UsageReportEntry[] | { error: string }; @@ -38,7 +48,11 @@ function getSessionMessage(message: WebSocketMessage): Record | return envelope.message as Record; } -function withUsageSupportFeature(message: WebSocketMessage, enabled: boolean): string | null { +function withUsageSupportFeature( + message: WebSocketMessage, + enabled: boolean, + providerUsageListOnly: boolean, +): string | null { const envelope = parseJson(message) as { type?: unknown; message?: { type?: unknown; payload?: Record }; @@ -59,7 +73,11 @@ function withUsageSupportFeature(message: WebSocketMessage, enabled: boolean): s ...envelope.message, payload: { ...payload, - features: { ...features, usageSources: enabled, providerUsageList: enabled }, + features: { + ...features, + usageSources: enabled && !providerUsageListOnly, + providerUsageList: enabled, + }, }, }, }); @@ -93,23 +111,30 @@ export async function installUsageReportsFixture( page: Page, options: UsageReportsFixtureOptions, ): Promise { - const listRequests: Array<{ forceRefresh: boolean; reportIds?: string[] }> = []; + const listRequests: UsageListRequest[] = []; const listCounter = createCounter(); const usageSupported = options.usageSupported ?? true; + const providerUsageListOnly = options.providerUsageListOnly ?? false; + const requestType = providerUsageListOnly + ? "provider.usage.list.request" + : "usage.list_reports.request"; - await page.routeWebSocket(daemonWsRoutePattern(), (ws) => { + const route = + options.port === undefined ? daemonWsRoutePattern() : wsRoutePatternForPort(`${options.port}`); + await page.routeWebSocket(route, (ws) => { const server = ws.connectToServer(); ws.onMessage((message) => { const request = getSessionMessage(message); const requestId = request?.requestId; - if (request?.type === "usage.list_reports.request" && typeof requestId === "string") { - listRequests.push({ + if (request?.type === requestType && typeof requestId === "string") { + const listRequest: UsageListRequest = { forceRefresh: request.forceRefresh === true, reportIds: Array.isArray(request.reportIds) ? (request.reportIds as string[]) : undefined, - }); + }; + listRequests.push(listRequest); const scripted = pick(options.lists ?? [[]], listRequests.length - 1); - const response = typeof scripted === "function" ? scripted() : scripted; + const response = typeof scripted === "function" ? scripted(listRequest) : scripted; if ("error" in response) { ws.send( JSON.stringify({ @@ -118,7 +143,7 @@ export async function installUsageReportsFixture( type: "rpc_error", payload: { requestId, - requestType: "usage.list_reports.request", + requestType, error: response.error, code: "transport", }, @@ -130,12 +155,23 @@ export async function installUsageReportsFixture( } const ids = Array.isArray(request.reportIds) ? request.reportIds : null; const reports = ids ? response.filter((entry) => ids.includes(entry.id)) : response; - ws.send( - JSON.stringify({ - type: "session", - message: { type: "usage.list_reports.response", payload: { requestId, reports } }, - }), - ); + const reply = providerUsageListOnly + ? { + type: "provider.usage.list.response", + payload: { + requestId, + fetchedAt: new Date().toISOString(), + providers: reports.map((entry) => ({ + providerId: entry.sourceId, + displayName: entry.sourceLabel, + fetchedAt: entry.fetchedAt, + ...entry.report, + planLabel: entry.report.planLabel ?? null, + })), + }, + } + : { type: "usage.list_reports.response", payload: { requestId, reports } }; + ws.send(JSON.stringify({ type: "session", message: reply })); listCounter.increment(); return; } @@ -144,7 +180,9 @@ export async function installUsageReportsFixture( server.onMessage((message) => { const serverInfo = - typeof message === "string" ? withUsageSupportFeature(message, usageSupported) : null; + typeof message === "string" + ? withUsageSupportFeature(message, usageSupported, providerUsageListOnly) + : null; ws.send(serverInfo ?? message); }); }); diff --git a/packages/app/src/components/hosts/host-filter.tsx b/packages/app/src/components/hosts/host-filter.tsx index ffdb6a2130..d073dc943d 100644 --- a/packages/app/src/components/hosts/host-filter.tsx +++ b/packages/app/src/components/hosts/host-filter.tsx @@ -2,13 +2,13 @@ import { useCallback, useMemo, useRef, useState, type ReactElement } from "react import { Pressable, Text, View, type PressableStateCallbackType } from "react-native"; import { ChevronDown, Server } from "lucide-react-native"; import { StyleSheet, withUnistyles } from "react-native-unistyles"; -import type { HostProfile } from "@/types/host-connection"; import type { Theme } from "@/styles/theme"; import { ALL_HOSTS_OPTION_ID, getHostPickerLabel, HostPicker, HostStatusDotSlot, + type HostPickerHost, } from "@/components/hosts/host-picker"; const ThemedServer = withUnistyles(Server); @@ -16,7 +16,7 @@ const ThemedChevronDown = withUnistyles(ChevronDown); const mutedColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted }); export interface HostFilterProps { - hosts: HostProfile[]; + hosts: HostPickerHost[]; selectedHost: string; onSelectHost: (serverId: string) => void; /** @@ -29,9 +29,9 @@ export interface HostFilterProps { } /** - * The "All hosts / " filter pill shared by the History and Schedules - * screens: an anchored HostPicker with `includeAllHost`, hidden by the caller - * when only one host exists. Copies the History layout exactly. + * The "All hosts / " host pill shared by History, Schedules, Usage, the label manager and + * plugin screens: an anchored HostPicker, hidden by the caller when only one host exists. Without + * `includeAllHost` it picks the one host a surface shows. */ export function HostFilter({ hosts, diff --git a/packages/app/src/components/hosts/host-picker.tsx b/packages/app/src/components/hosts/host-picker.tsx index 9bc8955e64..ced0be3d55 100644 --- a/packages/app/src/components/hosts/host-picker.tsx +++ b/packages/app/src/components/hosts/host-picker.tsx @@ -24,7 +24,7 @@ export { const SEARCHABLE_THRESHOLD = 10; type RenderHostOption = NonNullable; -interface HostPickerHost { +export interface HostPickerHost { serverId: string; label: string; } diff --git a/packages/app/src/components/hosts/host-switcher.tsx b/packages/app/src/components/hosts/host-switcher.tsx deleted file mode 100644 index 9172ab3e5d..0000000000 --- a/packages/app/src/components/hosts/host-switcher.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { ChevronDown } from "lucide-react-native"; -import { useCallback, useRef, useState } from "react"; -import { Pressable, Text, View } from "react-native"; -import { StyleSheet, withUnistyles } from "react-native-unistyles"; -import type { Theme } from "@/styles/theme"; -import { HostPicker } from "./host-picker"; - -const ThemedChevronDown = withUnistyles(ChevronDown); -const mutedColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted }); - -interface HostSwitcherHost { - serverId: string; - label: string; -} - -/** A compact trigger showing the chosen host, opening the host picker to change it. */ -export function HostSwitcher({ - hosts, - value, - onSelect, - title, - accessibilityLabel, - testID, -}: { - hosts: HostSwitcherHost[]; - value: string; - onSelect: (serverId: string) => void; - /** The picker's title. */ - title: string; - /** Prefixes the selected host's label: "Plugin host: Laptop". */ - accessibilityLabel: string; - testID: string; -}) { - const [open, setOpen] = useState(false); - const anchorRef = useRef(null); - const selectedLabel = hosts.find((host) => host.serverId === value)?.label ?? value; - const openPicker = useCallback(() => setOpen(true), []); - return ( - - - - - {selectedLabel} - - - - - - ); -} - -const styles = StyleSheet.create((theme) => ({ - trigger: { - maxWidth: 180, - minHeight: 32, - paddingHorizontal: theme.spacing[2], - flexDirection: "row", - alignItems: "center", - gap: theme.spacing[1], - borderRadius: theme.borderRadius.md, - backgroundColor: theme.colors.surface1, - }, - label: { - flexShrink: 1, - color: theme.colors.foregroundMuted, - fontSize: theme.fontSize.base, - }, -})); diff --git a/packages/app/src/components/left-sidebar.tsx b/packages/app/src/components/left-sidebar.tsx index b1de36c8ea..c2bff3febc 100644 --- a/packages/app/src/components/left-sidebar.tsx +++ b/packages/app/src/components/left-sidebar.tsx @@ -23,6 +23,7 @@ import { } from "@/components/sidebar-resize-handle-layout"; import { HostPicker } from "@/components/hosts/host-picker"; import { SidebarDisplayPreferencesMenu } from "@/components/sidebar/display-preferences/menu"; +import { SidebarSeparator } from "@/components/sidebar/sidebar-separator"; import { SidebarNavRows } from "@/components/sidebar/sidebar-nav-rows"; import { SidebarHelpMenu } from "@/components/sidebar/sidebar-help-menu"; import { SidebarResizeHandle } from "@/components/sidebar-resize-handle"; @@ -309,7 +310,7 @@ function FooterIconButton({ )} - + @@ -367,7 +368,7 @@ function FooterAddProjectButton({ }} - + @@ -473,37 +474,39 @@ function SidebarFooter({ const settingsKeys = useShortcutKeys("toggle-settings"); return ( - - + - - - - - + + + + + + + ); @@ -516,21 +519,24 @@ function SidebarFooterRows({ onBeforeNavigate }: { onBeforeNavigate?: () => void const visibleItems = items.filter((item) => item.visible); if (visibleItems.length === 0) return null; return ( - - {visibleItems.map((item) => - item.kind === "plugin" ? ( - - ) : ( - - ), - )} - + <> + + {visibleItems.map((item) => + item.kind === "plugin" ? ( + + ) : ( + + ), + )} + + + ); } @@ -961,32 +967,32 @@ const styles = StyleSheet.create((theme) => ({ fontSize: theme.fontSize.sm, fontWeight: theme.fontWeight.medium, }, - sidebarFooter: { - gap: theme.spacing[1], - paddingHorizontal: theme.spacing[2], - paddingVertical: theme.spacing[3], + footerContainer: { borderTopWidth: 1, borderTopColor: theme.colors.border, }, - // Every footer row is 28px tall with `spacing[1]` between rows, the same as the gap above. - footerRows: { - gap: theme.spacing[1], + sidebarFooter: { + flexDirection: "row", + alignItems: "center", + gap: theme.spacing[2], + paddingHorizontal: theme.spacing[2], + paddingVertical: theme.spacing[3], }, footerIconRow: { flexDirection: "row", alignItems: "center", gap: theme.spacing[2], + flexShrink: 0, }, - // The leading glyph sits on the footer icons' rail: a 16px icon centered in a 28px button - // starts 6px in, `spacing[1.5]`. footerAddProjectButton: { minWidth: 0, - minHeight: 28, + minHeight: 32, + flex: 1, flexDirection: "row", alignItems: "center", gap: theme.spacing[2], - paddingVertical: theme.spacing[1], - paddingHorizontal: theme.spacing[1.5], + paddingVertical: theme.spacing[1.5], + paddingHorizontal: theme.spacing[2], borderRadius: theme.borderRadius.lg, }, footerAddProjectButtonHovered: { @@ -1002,6 +1008,12 @@ const styles = StyleSheet.create((theme) => ({ footerAddProjectLabelHovered: { color: theme.colors.foreground, }, + // Usage and plugin rows sit above the restored footer line. + footerRows: { + paddingHorizontal: theme.spacing[2], + paddingVertical: theme.spacing[3], + gap: theme.spacing[1], + }, footerIconButton: { width: 28, height: 28, diff --git a/packages/app/src/components/sidebar/sidebar-separator.tsx b/packages/app/src/components/sidebar/sidebar-separator.tsx index 864e56361f..30310d84ad 100644 --- a/packages/app/src/components/sidebar/sidebar-separator.tsx +++ b/packages/app/src/components/sidebar/sidebar-separator.tsx @@ -6,8 +6,8 @@ import { StyleSheet } from "react-native-unistyles"; * padded container so the line runs the full sidebar width — matching the * separator beneath the workspace sidebar's "New agent" header. */ -export function SidebarSeparator() { - return ; +export function SidebarSeparator({ testID }: { testID?: string } = {}) { + return ; } const styles = StyleSheet.create((theme) => ({ diff --git a/packages/app/src/hooks/use-settings/storage.test.ts b/packages/app/src/hooks/use-settings/storage.test.ts index 3797d76e8b..9761256278 100644 --- a/packages/app/src/hooks/use-settings/storage.test.ts +++ b/packages/app/src/hooks/use-settings/storage.test.ts @@ -267,6 +267,7 @@ describe("loadAppSettingsFromStorage", () => { expect((await loadAppSettingsFromStorage(makeDeps())).usage).toEqual({ displayAs: "used", pinned: [], + serverId: null, }); const deps = makeDeps({ @@ -278,6 +279,7 @@ describe("loadAppSettingsFromStorage", () => { { sourceId: "codex", windowId: "weekly" }, { sourceId: "claude", windowId: "five-hour" }, ], + serverId: "server", }, }), }), @@ -289,6 +291,7 @@ describe("loadAppSettingsFromStorage", () => { { sourceId: "codex", windowId: "weekly" }, { sourceId: "claude", windowId: "five-hour" }, ], + serverId: "server", }); }); @@ -296,7 +299,11 @@ describe("loadAppSettingsFromStorage", () => { const deps = makeDeps({ storage: createInMemoryKeyValueStorage({ [APP_SETTINGS_KEY]: JSON.stringify({ - usage: { displayAs: "percent", pinned: [{ sourceId: "claude", windowId: "weekly" }] }, + usage: { + displayAs: "percent", + pinned: [{ sourceId: "claude", windowId: "weekly" }], + serverId: 42, + }, }), }), }); @@ -304,6 +311,7 @@ describe("loadAppSettingsFromStorage", () => { expect((await loadAppSettingsFromStorage(deps)).usage).toEqual({ displayAs: "used", pinned: [{ sourceId: "claude", windowId: "weekly" }], + serverId: null, }); }); diff --git a/packages/app/src/plugins/surface-screen.tsx b/packages/app/src/plugins/surface-screen.tsx index c6e7b0a6cc..f2f9997749 100644 --- a/packages/app/src/plugins/surface-screen.tsx +++ b/packages/app/src/plugins/surface-screen.tsx @@ -9,7 +9,7 @@ import { HeaderIconBadge } from "@/components/headers/header-icon-badge"; import { HeaderToggleButton } from "@/components/headers/header-toggle-button"; import { ScreenHeader } from "@/components/headers/screen-header"; import { ScreenTitle } from "@/components/headers/screen-title"; -import { HostSwitcher } from "@/components/hosts/host-switcher"; +import { HostFilter } from "@/components/hosts/host-filter"; import { useIsCompactFormFactor } from "@/constants/layout"; import { useHostRuntimeClient, useHosts } from "@/runtime/host-runtime"; import type { Theme } from "@/styles/theme"; @@ -82,7 +82,7 @@ function SurfaceRenderer({ const ThemedSurfaceRenderer = withUnistyles(SurfaceRenderer); -function PluginHostSwitcher({ +function PluginHostFilter({ serverId, pluginId, identity, @@ -117,13 +117,12 @@ function PluginHostSwitcher({ if (!show) return null; return ( - ); } @@ -192,7 +191,7 @@ export function PluginSurfaceScreen() { () => ( <> {identity ? ( - Sidebar: one card per section. The footer's Add project row and icon row are fixed and not listed. */ +/** Settings > Sidebar: one card per section. The footer's bottom line is fixed and not listed. */ export function SidebarNavSection(): ReactElement { return ( <> diff --git a/packages/app/src/sidebar-nav/model.test.ts b/packages/app/src/sidebar-nav/model.test.ts index 33604b72e9..4211a44410 100644 --- a/packages/app/src/sidebar-nav/model.test.ts +++ b/packages/app/src/sidebar-nav/model.test.ts @@ -299,6 +299,7 @@ describe("footer section", () => { section: "footer", pluginGroups: [sync], preferences: [ + { key: "add-project", visible: false }, { key: "hosts", visible: false }, { key: "import", visible: false }, { key: "help", visible: false }, diff --git a/packages/app/src/sidebar-nav/model.ts b/packages/app/src/sidebar-nav/model.ts index 9691ec8b80..40e4db636f 100644 --- a/packages/app/src/sidebar-nav/model.ts +++ b/packages/app/src/sidebar-nav/model.ts @@ -4,8 +4,8 @@ import type { PluginSidebarSection } from "@/plugins/types"; export type SidebarSection = PluginSidebarSection; /** - * Each section's built-in items in their default order. The footer's Add project row and its - * icon row (Hosts, Import session, Help and support, Settings) are fixed and not items. + * Each section's built-in items in their default order. The footer's bottom line + * (Add project and the Hosts, Import session, Help and support, Settings icons) is fixed. */ export const BUILTIN_SIDEBAR_ITEM_IDS = { header: ["new-workspace", "history", "search", "schedules"], diff --git a/packages/app/src/usage/controls.tsx b/packages/app/src/usage/controls.tsx index 582f35102b..b7942fddd9 100644 --- a/packages/app/src/usage/controls.tsx +++ b/packages/app/src/usage/controls.tsx @@ -1,7 +1,7 @@ import { RotateCw } from "lucide-react-native"; import { View } from "react-native"; import { StyleSheet, withUnistyles } from "react-native-unistyles"; -import { HostSwitcher } from "@/components/hosts/host-switcher"; +import { HostFilter } from "@/components/hosts/host-filter"; import { extraMutedIconColorMapping } from "@/components/ui/icon-button-chrome"; import { LoadingSpinner } from "@/components/ui/loading-spinner"; import { ToolbarButton, paneContentToolbarIconSize } from "@/components/ui/pane-content-toolbar"; @@ -25,9 +25,9 @@ export interface UsageHostSelection { } /** - * The controls on the right of every usage title row: the host selector, which also names the - * host shown, the used/remaining toggle and Refresh. A host that cannot report usage keeps only - * the selector. + * The controls on the right of every usage title row: the host filter when there is more than one + * host, the used/remaining toggle and Refresh. A host that cannot report usage keeps only the + * host filter. */ export function UsageControls({ view, @@ -45,14 +45,14 @@ export function UsageControls({ const iconSize = paneContentToolbarIconSize(compact); return ( - {hostSelection && hostSelection.hosts.length > 0 ? ( - 1 ? ( + ) : null} {view.kind === "unavailable" ? null : ( @@ -78,6 +78,10 @@ export function UsageControls({ ); } +function usageHostOptionTestID(serverId: string): string { + return `usage-host-filter-item-${serverId}`; +} + const styles = StyleSheet.create((theme) => ({ controls: { flexShrink: 1, diff --git a/packages/app/src/usage/copy.ts b/packages/app/src/usage/copy.ts index ec42581fbe..5d84805ea2 100644 --- a/packages/app/src/usage/copy.ts +++ b/packages/app/src/usage/copy.ts @@ -16,7 +16,6 @@ export const usageCopy = { clientUnavailable: "Host connection is not ready", retry: "Try again", pin: "Pin", - host: "Usage host", displayUsed: "Used", displayRemaining: "Remaining", } as const; diff --git a/packages/app/src/usage/hosts.ts b/packages/app/src/usage/hosts.ts index 5b6597ecdd..450e0a6157 100644 --- a/packages/app/src/usage/hosts.ts +++ b/packages/app/src/usage/hosts.ts @@ -1,9 +1,16 @@ -import { useMemo, useState } from "react"; +import { useCallback, useMemo } from "react"; +import { useAppSettings } from "@/hooks/use-settings"; import { useActiveWorkspaceSelection, useLastWorkspaceSelection, } from "@/stores/navigation-active-workspace-store"; -import { resolveUsageHostId, resolveUsageScreenHostId, type UsageHost } from "./model"; +import { + resolveUsageHostId, + resolveUsageScreenHostId, + type UsageHost, + type UsageHostChoice, +} from "./model"; +import { setUsageHost } from "./preferences"; import { useUsageHosts } from "./queries"; /** The active workspace's host; off a workspace route, the last workspace visited. */ @@ -13,27 +20,40 @@ function useActiveServerId(): string | null { return active?.serverId ?? last?.serverId ?? null; } +function useUsageHostChoice(): UsageHostChoice { + const { settings } = useAppSettings(); + return { + pickedServerId: settings.usage.serverId, + activeServerId: useActiveServerId(), + hosts: useUsageHosts(), + }; +} + /** The host the sidebar Usage row reads, or null when none reports usage. */ export function useUsageHostId(): string | null { - return resolveUsageHostId(useActiveServerId(), useUsageHosts()); + return resolveUsageHostId(useUsageHostChoice()); } /** - * The host the Usage screen or the compact usage sheet shows, and the hosts to pick from. The pick - * is the view's state: it resets when the view unmounts. + * The host the Usage screen or the compact usage sheet shows, and the hosts to pick from. A pick + * is saved on the device, so the sidebar Usage row and later visits show the same host. */ export function useUsageHostSelection(): { serverId: string | null; connectedHosts: UsageHost[]; select: (serverId: string) => void; } { - const hosts = useUsageHosts(); - const activeServerId = useActiveServerId(); - const [selectedServerId, select] = useState(null); - const connectedHosts = useMemo(() => hosts.filter((host) => host.isConnected), [hosts]); - return { - serverId: resolveUsageScreenHostId({ selectedServerId, activeServerId, hosts }), - connectedHosts, - select, - }; + const { updateSettings } = useAppSettings(); + const choice = useUsageHostChoice(); + const connectedHosts = useMemo( + () => choice.hosts.filter((host) => host.isConnected), + [choice.hosts], + ); + const select = useCallback( + (serverId: string) => { + void updateSettings((current) => ({ usage: setUsageHost(current.usage, serverId) })); + }, + [updateSettings], + ); + return { serverId: resolveUsageScreenHostId(choice), connectedHosts, select }; } diff --git a/packages/app/src/usage/model.test.ts b/packages/app/src/usage/model.test.ts index fd46a2e476..834746e1a7 100644 --- a/packages/app/src/usage/model.test.ts +++ b/packages/app/src/usage/model.test.ts @@ -84,52 +84,65 @@ const hosts: UsageHost[] = [ ]; describe("resolveUsageHostId", () => { - it("reads the active workspace's host", () => { - expect(resolveUsageHostId("b", hosts)).toBe("b"); + const choose = (pickedServerId: string | null, activeServerId: string | null) => + resolveUsageHostId({ pickedServerId, activeServerId, hosts }); + + it("reads the picked host over the active workspace's host", () => { + expect(choose("a", "b")).toBe("a"); + }); + + it("reads the active workspace's host when nothing usable is picked", () => { + expect(choose(null, "b")).toBe("b"); + expect(choose("offline", "b")).toBe("b"); + expect(choose("old", "b")).toBe("b"); }); it("falls back to the first connected host that reports usage", () => { - expect(resolveUsageHostId(null, hosts)).toBe("a"); - expect(resolveUsageHostId("offline", hosts)).toBe("a"); - expect(resolveUsageHostId("old", hosts)).toBe("a"); + expect(choose(null, null)).toBe("a"); + expect(choose(null, "offline")).toBe("a"); + expect(choose(null, "old")).toBe("a"); }); it("has no host when none reports usage", () => { - expect(resolveUsageHostId("old", hosts.slice(0, 2))).toBeNull(); + expect( + resolveUsageHostId({ + pickedServerId: "old", + activeServerId: "old", + hosts: hosts.slice(0, 2), + }), + ).toBeNull(); }); }); describe("resolveUsageScreenHostId", () => { - it("keeps the user's pick while it stays connected", () => { - expect(resolveUsageScreenHostId({ selectedServerId: "old", activeServerId: "b", hosts })).toBe( + it("shows the picked host while it stays connected, even one that cannot report usage", () => { + expect(resolveUsageScreenHostId({ pickedServerId: "old", activeServerId: "b", hosts })).toBe( "old", ); expect( - resolveUsageScreenHostId({ selectedServerId: "offline", activeServerId: "b", hosts }), + resolveUsageScreenHostId({ pickedServerId: "offline", activeServerId: "b", hosts }), ).toBe("b"); }); - it("defaults to the host the sidebar row reads", () => { - for (const activeServerId of ["b", null, "old", "offline"]) { - expect(resolveUsageScreenHostId({ selectedServerId: null, activeServerId, hosts })).toBe( - resolveUsageHostId(activeServerId, hosts), - ); + it("otherwise shows the host the sidebar row reads", () => { + for (const pickedServerId of [null, "a", "offline"]) { + for (const activeServerId of ["b", null, "old", "offline"]) { + const choice = { pickedServerId, activeServerId, hosts }; + expect(resolveUsageScreenHostId(choice)).toBe(resolveUsageHostId(choice)); + } } - expect(resolveUsageScreenHostId({ selectedServerId: null, activeServerId: "old", hosts })).toBe( - "a", - ); }); it("shows the first connected host when none reports usage, so the screen says to update it", () => { expect( resolveUsageScreenHostId({ - selectedServerId: null, + pickedServerId: null, activeServerId: null, hosts: hosts.slice(0, 2), }), ).toBe("old"); expect( - resolveUsageScreenHostId({ selectedServerId: null, activeServerId: null, hosts: [] }), + resolveUsageScreenHostId({ pickedServerId: null, activeServerId: null, hosts: [] }), ).toBeNull(); }); }); diff --git a/packages/app/src/usage/model.ts b/packages/app/src/usage/model.ts index ad5fe99c2f..0e44ddae18 100644 --- a/packages/app/src/usage/model.ts +++ b/packages/app/src/usage/model.ts @@ -97,34 +97,32 @@ export interface UsageHost { supportsUsage: boolean; } -/** - * The host usage shows by default, on the sidebar row and the Usage screen: the active workspace's - * host if it reports usage, else the first host that does. - */ -export function resolveUsageHostId( - activeServerId: string | null, - hosts: readonly UsageHost[], -): string | null { - const reporting = hosts.filter((host) => host.isConnected && host.supportsUsage); - const active = reporting.find((host) => host.serverId === activeServerId); - return (active ?? reporting[0])?.serverId ?? null; +/** Where usage looks for its host: the user's saved pick, then the workspace they are in. */ +export interface UsageHostChoice { + pickedServerId: string | null; + activeServerId: string | null; + hosts: readonly UsageHost[]; } /** - * The host the Usage screen shows: the user's pick while it stays connected, else the default - * host. With no host reporting usage, the first connected host, so the screen says to update it. + * The host the sidebar Usage row reads: the picked host, else the active workspace's host, else the + * first host. Each only while it is connected and reports usage. */ -export function resolveUsageScreenHostId(input: { - selectedServerId: string | null; - activeServerId: string | null; - hosts: readonly UsageHost[]; -}): string | null { - const connected = input.hosts.filter((host) => host.isConnected); - const selected = connected.find((host) => host.serverId === input.selectedServerId); +export function resolveUsageHostId(choice: UsageHostChoice): string | null { + const reporting = choice.hosts.filter((host) => host.isConnected && host.supportsUsage); + const find = (serverId: string | null) => reporting.find((host) => host.serverId === serverId); return ( - selected?.serverId ?? - resolveUsageHostId(input.activeServerId, input.hosts) ?? - connected[0]?.serverId ?? - null + (find(choice.pickedServerId) ?? find(choice.activeServerId) ?? reporting[0])?.serverId ?? null ); } + +/** + * The host the Usage screen shows: the picked host while it is connected, even one that cannot + * report usage so the screen says to update it; else the sidebar row's host; else the first + * connected host. + */ +export function resolveUsageScreenHostId(choice: UsageHostChoice): string | null { + const connected = choice.hosts.filter((host) => host.isConnected); + const picked = connected.find((host) => host.serverId === choice.pickedServerId); + return picked?.serverId ?? resolveUsageHostId(choice) ?? connected[0]?.serverId ?? null; +} diff --git a/packages/app/src/usage/pinned.test.ts b/packages/app/src/usage/pinned.test.ts index 0580d3ccfc..af6d48cb12 100644 --- a/packages/app/src/usage/pinned.test.ts +++ b/packages/app/src/usage/pinned.test.ts @@ -40,10 +40,50 @@ function preferences( pinned: UsagePreferences["pinned"], displayAs: UsagePreferences["displayAs"] = "used", ): UsagePreferences { - return { displayAs, pinned }; + return { displayAs, pinned, serverId: null }; } describe("resolvePinnedUsage", () => { + it("defaults to the first window with a percent for each source account", () => { + const work = report({ + sourceId: "claude", + sourceLabel: "Claude", + account: "work", + windows: [ + { id: "empty", label: "Empty" }, + { id: "weekly", label: "Weekly", usedPct: 90 }, + ], + }); + const empty = report({ + sourceId: "empty", + sourceLabel: "Empty", + windows: [{ id: "empty", label: "Empty" }], + }); + expect( + resolvePinnedUsage([claude, codex, work, empty], preferences([])).map((item) => [ + item.key, + item.percentText, + ]), + ).toEqual([ + ["claude:default/five-hour", "31%"], + ["codex:default/weekly", "12%"], + ["claude:work/weekly", "90%"], + ]); + }); + + it("pins replace defaults, and removing all pins restores defaults", () => { + const reports = [claude, codex]; + expect( + resolvePinnedUsage(reports, preferences([{ sourceId: "claude", windowId: "weekly" }])).map( + (item) => item.key, + ), + ).toEqual(["claude:default/weekly"]); + expect(resolvePinnedUsage(reports, preferences([])).map((item) => item.key)).toEqual([ + "claude:default/five-hour", + "codex:default/weekly", + ]); + }); + it("shows pinned windows in pin order, not report order", () => { const items = resolvePinnedUsage( [claude, codex], @@ -54,11 +94,16 @@ describe("resolvePinnedUsage", () => { ); expect(items).toEqual([ - { key: "codex:default/weekly", icon: null, label: "Codex Weekly", percentText: "12%" }, + { + key: "codex:default/weekly", + icon: null, + label: "Codex Weekly 12% used", + percentText: "12%", + }, { key: "claude:default/five-hour", icon: "", - label: "Claude 5-hour", + label: "Claude 5-hour 31% used", percentText: "31%", }, ]); @@ -76,7 +121,11 @@ describe("resolvePinnedUsage", () => { ), ); - expect(items.map((item) => item.percentText)).toEqual(["69% left", "88% left"]); + expect(items.map((item) => item.percentText)).toEqual(["69%", "88%"]); + expect(items.map((item) => item.label)).toEqual([ + "Claude 5-hour 69% left", + "Codex Weekly 88% left", + ]); }); it("leaves out a pinned window that no report has, or that reports no percent", () => { @@ -116,8 +165,8 @@ describe("resolvePinnedUsage", () => { ); expect(items.map((item) => [item.label, item.percentText])).toEqual([ - ["Claude (personal) 5-hour", "10%"], - ["Claude (work) 5-hour", "90%"], + ["Claude (personal) 5-hour 10% used", "10%"], + ["Claude (work) 5-hour 90% used", "90%"], ]); }); }); diff --git a/packages/app/src/usage/pinned.ts b/packages/app/src/usage/pinned.ts index 7d348ffcd2..11cf0ecabf 100644 --- a/packages/app/src/usage/pinned.ts +++ b/packages/app/src/usage/pinned.ts @@ -1,13 +1,13 @@ -import { formatDisplayPct } from "./format"; +import { formatPct } from "./format"; import { displayPercent } from "./model"; import type { UsagePreferences } from "./preferences"; import type { UsageReportEntry } from "./types"; -/** One pinned window of one account, as the sidebar Usage item shows it. */ +/** One summary window of one account, as the sidebar Usage item shows it. */ export interface PinnedUsageWindow { key: string; icon: string | null; - /** Source, account when it has one, and window: "Claude (work) 5-hour". */ + /** Accessible description, including the source, account, window and percent meaning. */ label: string; percentText: string; } @@ -17,28 +17,40 @@ function describe(entry: UsageReportEntry, windowLabel: string): string { return `${entry.sourceLabel}${account} ${windowLabel}`; } -/** - * The pinned windows the Usage item shows: pins in pin order, and within a pin one item per account of the source - * in report order. A pinned window a report lacks, or reports no percent for, is left out. - */ +/** Pins replace defaults; without pins, summarize each account's first window with a percent. */ export function resolvePinnedUsage( reports: readonly UsageReportEntry[], preferences: UsagePreferences, ): PinnedUsageWindow[] { - return preferences.pinned.flatMap((pin) => - reports.flatMap((entry) => { - if (entry.sourceId !== pin.sourceId) return []; - const window = entry.report.windows.find((candidate) => candidate.id === pin.windowId); - const percent = window ? displayPercent(window, preferences.displayAs) : null; - if (!window || percent == null) return []; - return [ - { - key: `${entry.id}/${window.id}`, - icon: entry.icon ?? null, - label: describe(entry, window.label), - percentText: formatDisplayPct(percent, preferences.displayAs), - }, - ]; - }), - ); + const selections = + preferences.pinned.length === 0 + ? reports.map((entry) => ({ + entry, + window: entry.report.windows.find( + (window) => displayPercent(window, preferences.displayAs) !== null, + ), + })) + : preferences.pinned.flatMap((pin) => + reports + .filter((entry) => entry.sourceId === pin.sourceId) + .map((entry) => ({ + entry, + window: entry.report.windows.find((window) => window.id === pin.windowId), + })), + ); + return selections.flatMap(({ entry, window }) => { + if (!window) return []; + const percent = displayPercent(window, preferences.displayAs); + if (percent === null) return []; + const percentText = formatPct(percent); + const meaning = preferences.displayAs === "remaining" ? "left" : "used"; + return [ + { + key: `${entry.id}/${window.id}`, + icon: entry.icon ?? null, + label: `${describe(entry, window.label)} ${percentText} ${meaning}`, + percentText, + }, + ]; + }); } diff --git a/packages/app/src/usage/preferences.test.ts b/packages/app/src/usage/preferences.test.ts index b060632067..391ec05cd3 100644 --- a/packages/app/src/usage/preferences.test.ts +++ b/packages/app/src/usage/preferences.test.ts @@ -3,7 +3,9 @@ import { DEFAULT_USAGE_PREFERENCES, isUsagePinned, setUsageDisplayAs, + setUsageHost, toggleUsagePin, + UsagePreferencesSchema, } from "./preferences"; const claudeFiveHour = { sourceId: "claude", windowId: "five-hour" }; @@ -23,17 +25,33 @@ describe("usage preferences", () => { it("unpins a pinned window and keeps the others in order", () => { const three = [codexWeekly, claudeFiveHour, { sourceId: "claude", windowId: "weekly" }]; - const unpinned = toggleUsagePin({ displayAs: "used", pinned: three }, claudeFiveHour); + const unpinned = toggleUsagePin( + { displayAs: "used", pinned: three, serverId: null }, + claudeFiveHour, + ); expect(unpinned.pinned).toEqual([codexWeekly, { sourceId: "claude", windowId: "weekly" }]); }); it("switches between used and remaining without touching pins", () => { - const preferences = { displayAs: "used" as const, pinned: [codexWeekly] }; + const preferences = { displayAs: "used" as const, pinned: [codexWeekly], serverId: null }; expect(setUsageDisplayAs(preferences, "remaining")).toEqual({ displayAs: "remaining", pinned: [codexWeekly], + serverId: null, }); }); + + it("remembers the picked host without touching pins", () => { + expect(setUsageHost({ ...DEFAULT_USAGE_PREFERENCES, pinned: [codexWeekly] }, "server")).toEqual( + { displayAs: "used", pinned: [codexWeekly], serverId: "server" }, + ); + }); + + it("reads preferences saved before hosts could be picked as no pick", () => { + expect(UsagePreferencesSchema.parse({ displayAs: "remaining", pinned: [codexWeekly] })).toEqual( + { displayAs: "remaining", pinned: [codexWeekly], serverId: null }, + ); + }); }); diff --git a/packages/app/src/usage/preferences.ts b/packages/app/src/usage/preferences.ts index 55c5ee7d8e..c25cf6e1c7 100644 --- a/packages/app/src/usage/preferences.ts +++ b/packages/app/src/usage/preferences.ts @@ -13,14 +13,21 @@ export interface UsagePin { export interface UsagePreferences { displayAs: UsageDisplayAs; pinned: UsagePin[]; + /** The host the user picked to show usage for; null until they pick one. */ + serverId: string | null; } -export const DEFAULT_USAGE_PREFERENCES: UsagePreferences = { displayAs: "used", pinned: [] }; +export const DEFAULT_USAGE_PREFERENCES: UsagePreferences = { + displayAs: "used", + pinned: [], + serverId: null, +}; export const UsagePreferencesSchema = z .object({ displayAs: z.enum(["used", "remaining"]).catch("used"), pinned: z.array(z.object({ sourceId: z.string(), windowId: z.string() })).catch([]), + serverId: z.string().nullable().catch(null), }) .catch(DEFAULT_USAGE_PREFERENCES); @@ -46,3 +53,7 @@ export function setUsageDisplayAs( ): UsagePreferences { return { ...preferences, displayAs }; } + +export function setUsageHost(preferences: UsagePreferences, serverId: string): UsagePreferences { + return { ...preferences, serverId }; +} diff --git a/packages/app/src/usage/sidebar-item.tsx b/packages/app/src/usage/sidebar-item.tsx index c4993b8b2e..9b9e9a80ef 100644 --- a/packages/app/src/usage/sidebar-item.tsx +++ b/packages/app/src/usage/sidebar-item.tsx @@ -25,15 +25,14 @@ const NO_REPORTS: UsageReportEntry[] = []; const NO_ITEMS: PinnedUsageWindow[] = []; /** - * The sidebar footer's usage entry: each pinned window's source icon and percent, or a plain - * "Usage" row while no pinned window has data. Pressing it opens the Usage screen; on compact + * The sidebar footer's usage entry: each summary window's source icon and percent, or a plain + * "Usage" row while no summary window has data. Pressing it opens the Usage screen; on compact * layouts it opens the usage sheet instead. */ export function UsageSidebarItem() { const { preferences, display } = useUsagePreferences(); const serverId = useUsageHostId(); - // Without pins there is nothing to summarize, so the host is not asked for reports. - if (!serverId || preferences.pinned.length === 0) { + if (!serverId) { return ; } return ( @@ -84,7 +83,7 @@ function UsageEntry({ const isCompact = useIsCompactFormFactor(); const openUsageScreen = useOpenUsageScreen(); const [open, setOpen] = useState(false); - // The sheet mounts on first open, so an unopened sidebar never asks the host for reports. + // The sheet mounts on first open; the summary already owns the report query. const [sheetMounted, setSheetMounted] = useState(false); // Without a host there are no reports to show, so compact goes to the screen, which says so. const usesSheet = isCompact && serverId !== null; @@ -170,7 +169,7 @@ function HostUsageSheet({ } function pinnedUsageLabel(label: string, items: readonly PinnedUsageWindow[]): string { - return `${label}: ${items.map((item) => `${item.label} ${item.percentText}`).join(", ")}`; + return `${label}: ${items.map((item) => item.label).join(", ")}`; } function triggerStyle({ hovered }: PressableStateCallbackType & { hovered?: boolean }) { diff --git a/packages/client/src/daemon-client.test.ts b/packages/client/src/daemon-client.test.ts index 0f6d7f1e07..3554fb8a0e 100644 --- a/packages/client/src/daemon-client.test.ts +++ b/packages/client/src/daemon-client.test.ts @@ -1,3 +1,5 @@ +import { legacyUsageIcon } from "./legacy-usage-icons.js"; +import { readFileSync } from "node:fs"; import { afterEach, expect, expectTypeOf, test, vi } from "vitest"; import { z } from "zod"; import { @@ -6573,6 +6575,22 @@ test("sends provider.usage.list.request and resolves provider.usage.list.respons }); }); +test.each(["claude", "codex", "copilot", "cursor", "zai", "grok", "kimi", "minimax"])( + "released-host %s icon matches its source plugin", + (sourceId) => { + const icon = readFileSync( + new URL(`../../../plugins/${sourceId}-usage-source/icon.svg`, import.meta.url), + "utf8", + ); + expect(legacyUsageIcon(sourceId)).toBe(icon); + }, +); + +test("unknown released-host sources have no icon", () => { + expect(legacyUsageIcon("unknown")).toBeUndefined(); + expect(legacyUsageIcon("toString")).toBeUndefined(); +}); + test("maps released-host usage reports and filters report IDs", async () => { const mock = createMockTransport(); const client = new DaemonClient({ @@ -6608,11 +6626,19 @@ test("maps released-host usage reports and filters report IDs", async () => { error: "unavailable", }, ]; + const extraProviders = ["copilot", "cursor", "zai", "grok", "kimi", "minimax", "unknown"].map( + (providerId) => Object.assign({}, providers[0]!, { providerId, displayName: providerId }), + ); + providers.push(...extraProviders); const reports = [ { id: "claude", sourceId: "claude", sourceLabel: "Claude", + icon: readFileSync( + new URL("../../../plugins/claude-usage-source/icon.svg", import.meta.url), + "utf8", + ), account: {}, fetchedAt: "2026-09-30T00:00:00.000Z", report: { @@ -6628,6 +6654,10 @@ test("maps released-host usage reports and filters report IDs", async () => { id: "codex", sourceId: "codex", sourceLabel: "Codex", + icon: readFileSync( + new URL("../../../plugins/codex-usage-source/icon.svg", import.meta.url), + "utf8", + ), account: {}, fetchedAt: "2026-09-29T00:00:00.000Z", report: { @@ -6640,6 +6670,16 @@ test("maps released-host usage reports and filters report IDs", async () => { }, }, ]; + reports.push( + ...extraProviders.map((provider) => + Object.assign({}, reports[0]!, { + id: provider.providerId, + sourceId: provider.providerId, + sourceLabel: provider.displayName, + icon: legacyUsageIcon(provider.providerId), + }), + ), + ); for (const reportIds of [undefined, ["codex", "missing"], []]) { const result = client.listUsageReports({ requestId: "legacy-usage", diff --git a/packages/client/src/daemon-client.ts b/packages/client/src/daemon-client.ts index ba1e09e7cb..145aad7447 100644 --- a/packages/client/src/daemon-client.ts +++ b/packages/client/src/daemon-client.ts @@ -1,3 +1,4 @@ +import { legacyUsageIcon } from "./legacy-usage-icons.js"; import { subscribeTimeline, type TimelineMessage } from "./timeline-subscription/index.js"; import { ProviderSnapshotUpdates } from "./provider-snapshots/index.js"; import { @@ -5281,6 +5282,7 @@ export class DaemonClient { id: provider.providerId, sourceId: provider.providerId, sourceLabel: provider.displayName, + icon: legacyUsageIcon(provider.providerId), account: {}, fetchedAt: provider.fetchedAt ?? payload.fetchedAt, report: { diff --git a/packages/client/src/legacy-usage-icons.ts b/packages/client/src/legacy-usage-icons.ts new file mode 100644 index 0000000000..227bde8134 --- /dev/null +++ b/packages/client/src/legacy-usage-icons.ts @@ -0,0 +1,21 @@ +// COMPAT(providerUsageList): added in v0.1.98, remove after 2027-03-26. +// Released hosts omit source icons. Delete these copies with the list conversion. +const ICONS: Readonly> = { + claude: + '\n', + codex: + '\n', + copilot: + '\n', + cursor: + '\n \n\n', + zai: 'Z.ai\n', + grok: '\n', + kimi: '\n \n\n', + minimax: + '\n', +}; + +export function legacyUsageIcon(sourceId: string): string | undefined { + return Object.hasOwn(ICONS, sourceId) ? ICONS[sourceId] : undefined; +} From a7c9c05bef06f1b0aea14653e7b14c315d27918d Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Wed, 30 Sep 2026 22:59:33 +0200 Subject: [PATCH 033/100] Start new workspaces off origin/main at the remote tip (#5788) * fix(server): start new worktrees at the remote tip of an origin/* base Branching off a remote-tracking base such as origin/main used whatever the repository had cached. Nothing on the branch-off path fetched, and the background fetch only runs while a workspace of that repository is observed, so a new workspace could start hours or weeks behind the remote. Fetch just the base branch from its remote before git worktree add. The refspec is passed on the command line so remote..fetch is untouched, and a failed fetch falls back to the cached ref so creation still works offline. Co-authored-by: odoo-mvds * fix(server): fall back to the cached base before the create request times out A remote that accepts the connection and never answers held the base-ref fetch for its 120s cap, past the 60s client request timeout, so paseo worktree create failed and the worktree appeared later. Cap the fetch at 15s so creation falls back to the cached ref in time. --------- Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> Co-authored-by: odoo-mvds --- packages/server/src/utils/worktree.test.ts | 113 +++++++++++++++++++++ packages/server/src/utils/worktree.ts | 36 +++++++ 2 files changed, 149 insertions(+) diff --git a/packages/server/src/utils/worktree.test.ts b/packages/server/src/utils/worktree.test.ts index 4769bed7d0..479d4cfeab 100644 --- a/packages/server/src/utils/worktree.test.ts +++ b/packages/server/src/utils/worktree.test.ts @@ -20,6 +20,7 @@ import { writeFileSync, } from "fs"; import { join } from "path"; +import { createServer, type AddressInfo, type Socket } from "net"; import { tmpdir } from "os"; import { createRealpathAwarePathMatcher } from "./path"; @@ -52,6 +53,21 @@ function createLegacyWorktreeForTest( }); } +// A remote that accepts connections and never answers, like a VPN-only host while off the VPN. +async function startSilentRemote(): Promise<{ url: string; close: () => Promise }> { + const connections = new Set(); + const server = createServer((socket) => connections.add(socket)); + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + const { port } = server.address() as AddressInfo; + return { + url: `http://127.0.0.1:${port}/repo.git`, + close: () => { + for (const socket of connections) socket.destroy(); + return new Promise((resolve) => server.close(() => resolve())); + }, + }; +} + describe("paseo worktree manager", () => { let tempDir: string; let repoDir: string; @@ -263,6 +279,103 @@ describe("paseo worktree manager", () => { expect(existsSync(created.worktreePath)).toBe(false); }); + + describe("branch-off from a remote-tracking base", () => { + function git(args: string[], cwd: string): string { + return execFileSync("git", ["-c", "commit.gpgsign=false", ...args], { + cwd, + encoding: "utf8", + }).trim(); + } + + // The repo caches /main at "initial" while the remote itself moves one commit ahead, + // as when nothing has fetched since someone else pushed. + function pushPastCachedRemoteRef(remoteName: string): string { + const remoteDir = join(tempDir, "remote.git"); + git(["clone", "--bare", "-q", repoDir, remoteDir], tempDir); + git(["remote", "add", remoteName, remoteDir], repoDir); + git(["fetch", "-q", remoteName], repoDir); + + const pusherDir = join(tempDir, "pusher"); + git(["clone", "-q", remoteDir, pusherDir], tempDir); + git(["config", "user.email", "test@test.com"], pusherDir); + git(["config", "user.name", "Test"], pusherDir); + writeFileSync(join(pusherDir, "file.txt"), "pushed after the last fetch\n"); + git(["commit", "-q", "-am", "remote ahead"], pusherDir); + git(["push", "-q", "origin", "HEAD:main"], pusherDir); + return git(["rev-parse", "HEAD"], pusherDir); + } + + it("starts the new branch at the remote tip, not the cached origin/main", async () => { + const remoteTip = pushPastCachedRemoteRef("origin"); + const fetchRefspecs = git(["config", "--get-all", "remote.origin.fetch"], repoDir); + + const created = await createLegacyWorktreeForTest({ + branchName: "from-remote-tip", + cwd: repoDir, + baseBranch: "origin/main", + worktreeSlug: "from-remote-tip", + paseoHome, + }); + + expect(git(["rev-parse", "HEAD"], created.worktreePath)).toBe(remoteTip); + expect(git(["config", "--get-all", "remote.origin.fetch"], repoDir)).toBe(fetchRefspecs); + }); + + it("refreshes a base on a remote other than origin", async () => { + const remoteTip = pushPastCachedRemoteRef("team/upstream"); + + const created = await createLegacyWorktreeForTest({ + branchName: "from-upstream-tip", + cwd: repoDir, + baseBranch: "refs/remotes/team/upstream/main", + worktreeSlug: "from-upstream-tip", + paseoHome, + }); + + expect(git(["rev-parse", "HEAD"], created.worktreePath)).toBe(remoteTip); + }); + + it("branches from the cached ref when the remote is unreachable", async () => { + pushPastCachedRemoteRef("origin"); + const cachedTip = git(["rev-parse", "refs/remotes/origin/main"], repoDir); + git(["remote", "set-url", "origin", join(tempDir, "missing.git")], repoDir); + + const created = await createLegacyWorktreeForTest({ + branchName: "from-cached-ref", + cwd: repoDir, + baseBranch: "origin/main", + worktreeSlug: "from-cached-ref", + paseoHome, + }); + + expect(git(["rev-parse", "HEAD"], created.worktreePath)).toBe(cachedTip); + }); + + it("branches from the cached ref when the remote accepts but never answers", async () => { + pushPastCachedRemoteRef("origin"); + const cachedTip = git(["rev-parse", "refs/remotes/origin/main"], repoDir); + const silentRemote = await startSilentRemote(); + git(["remote", "set-url", "origin", silentRemote.url], repoDir); + + try { + const startedAt = Date.now(); + const created = await createLegacyWorktreeForTest({ + branchName: "from-silent-remote", + cwd: repoDir, + baseBranch: "origin/main", + worktreeSlug: "from-silent-remote", + paseoHome, + }); + + // Clients give up on a create request after 60s, so the fallback has to land well before. + expect(Date.now() - startedAt).toBeLessThan(30_000); + expect(git(["rev-parse", "HEAD"], created.worktreePath)).toBe(cachedTip); + } finally { + await silentRemote.close(); + } + }, 45_000); + }); }); describe("slugify", () => { diff --git a/packages/server/src/utils/worktree.ts b/packages/server/src/utils/worktree.ts index 0fd83e8dc7..f7f28205e0 100644 --- a/packages/server/src/utils/worktree.ts +++ b/packages/server/src/utils/worktree.ts @@ -1368,6 +1368,7 @@ async function resolveBranchOffWorktreeSourcePlan( const resolvedBaseBranch = await resolveBaseBranchForWorktree(cwd, source.baseBranch); const branchExists = await localBranchExists(cwd, branchName); const base = branchExists ? branchName : resolvedBaseBranch; + await refreshRemoteTrackingBaseRef(cwd, base); const candidateBranch = branchExists ? desiredSlug : branchName; const newBranchName = await resolveUniqueLocalBranchName(cwd, candidateBranch); @@ -1702,6 +1703,41 @@ async function resolveBaseBranchForWorktree( throw new Error(`Base branch not found: ${normalized}`); } +// Clients give up on a create request after 60s. A remote that accepts the connection and never +// answers (a VPN-only host while off the VPN) must fall back to the cached ref well before that. +const REMOTE_BASE_REFRESH_TIMEOUT_MS = 15_000; + +// A remote-tracking base is only as fresh as the last fetch, and the background fetch only runs +// while a workspace of the repository is observed. Refresh the one base branch so the new branch +// starts at the remote tip; when the fetch fails (offline, auth) the cached ref is still usable. +async function refreshRemoteTrackingBaseRef(cwd: string, baseRef: string): Promise { + if (!baseRef.startsWith("refs/remotes/")) { + return; + } + try { + // Remote names may contain slashes, so the owning remote is looked up rather than parsed. + const { stdout } = await runGitCommand(["remote"], { cwd }); + const remoteName = stdout + .split("\n") + .map((name) => name.trim()) + .find((name) => name.length > 0 && baseRef.startsWith(`refs/remotes/${name}/`)); + if (!remoteName) { + return; + } + const remoteBranch = baseRef.slice(`refs/remotes/${remoteName}/`.length); + // No refspec is added to remote..fetch: one left behind breaks later fetches once the + // branch is deleted on the remote. + await runGitCommand(["fetch", remoteName, `+refs/heads/${remoteBranch}:${baseRef}`], { + cwd, + envOverlay: { GIT_TERMINAL_PROMPT: "0" }, + timeout: REMOTE_BASE_REFRESH_TIMEOUT_MS, + acceptExitCodes: [0, 1, 128], + }); + } catch { + // The fetch timed out; branch from the cached ref. + } +} + async function ensureLocalBranch(cwd: string, branchName: string): Promise { if (await localBranchExists(cwd, branchName)) { return; From 20a887f4cc80db25a3027e50a576d45216f52b10 Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Thu, 1 Oct 2026 00:00:16 +0200 Subject: [PATCH 034/100] Show an agent's own model in the composer when the catalog lacks it (#5796) The composer fell back to the catalog's default model whenever the agent's model was missing from the loaded model list, so a Codex subagent started on a model the catalog did not list yet was labeled as the default model, with that model's thinking options. Fixes #5791 Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> --- .../src/composer/agent-controls/utils.test.ts | 26 +++++++++++++++++++ .../app/src/composer/agent-controls/utils.ts | 6 ++--- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/packages/app/src/composer/agent-controls/utils.test.ts b/packages/app/src/composer/agent-controls/utils.test.ts index 5b566fdeb4..993e21f0c9 100644 --- a/packages/app/src/composer/agent-controls/utils.test.ts +++ b/packages/app/src/composer/agent-controls/utils.test.ts @@ -186,4 +186,30 @@ describe("resolveAgentModelSelection", () => { expect(selection.selectedThinkingId).toBe("low"); expect(selection.displayThinking).toBe("Low"); }); + + it("shows the agent's own model when the catalog does not list it", () => { + const selection = resolveAgentModelSelection({ + models: [ + { + id: "gpt-6-sol", + provider: "codex", + label: "GPT-6-Sol", + isDefault: true, + thinkingOptions: [ + { id: "medium", label: "medium" }, + { id: "max", label: "max" }, + ], + defaultThinkingOptionId: "medium", + }, + ], + runtimeModelId: "gpt-6.1-sol", + configuredModelId: "gpt-6.1-sol", + explicitThinkingOptionId: "max", + }); + + expect(selection.selectedModel).toBeNull(); + expect(selection.activeModelId).toBe("gpt-6.1-sol"); + expect(selection.displayModel).toBe("gpt-6.1-sol"); + expect(selection.displayThinking).toBe("Max"); + }); }); diff --git a/packages/app/src/composer/agent-controls/utils.ts b/packages/app/src/composer/agent-controls/utils.ts index b28b693365..56f9bcd49f 100644 --- a/packages/app/src/composer/agent-controls/utils.ts +++ b/packages/app/src/composer/agent-controls/utils.ts @@ -79,10 +79,10 @@ function pickSelectedModel( preferredModelId: string | null, fallbackModel: AgentModelDefinition | null, ): AgentModelDefinition | null { - if (!models || !preferredModelId) { + if (!preferredModelId) { return fallbackModel; } - return findModelById(models, preferredModelId) ?? fallbackModel; + return findModelById(models, preferredModelId); } function resolveThinkingId( @@ -164,7 +164,7 @@ export function resolveAgentModelSelection(input: { const thinkingOptions = selectedModel?.thinkingOptions ?? null; const resolvedThinkingId = resolveThinkingId(explicitThinkingOptionId, selectedModel); const effectiveThinking = resolveEffectiveThinking(thinkingOptions, resolvedThinkingId); - const selectedThinkingId = effectiveThinking?.id ?? null; + const selectedThinkingId = effectiveThinking?.id ?? resolvedThinkingId; const displayThinking = resolveThinkingDisplay( effectiveThinking, selectedThinkingId, From 7b0cace991f32aba9e36f714be1921219a091843 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Wed, 30 Sep 2026 23:18:05 +0100 Subject: [PATCH 035/100] Restore Antigravity with Full access and session warnings (#5795) * Revert "Remove the unreleased Antigravity provider plugin (#5781)" This reverts commit 9d9d9af993afcefb4bc0f61dbe929bc7267d81f8. * fix(providers): commit plugin notices emitted during session opening * feat(providers): restore Antigravity with full access and session warnings * fix(providers): warn once when opening Antigravity sessions Driver respawn retains the live transcript and unchanged permission posture. Emit the warning only when opening a session, including resume after a daemon restart. --- CLAUDE.md | 2 +- README.md | 5 +- docs/providers.md | 9 +- .../src/server/agent/plugin-provider.test.ts | 50 ++ .../src/server/agent/plugin-provider.ts | 7 + .../src/server/daemon-e2e/models.e2e.test.ts | 69 ++ .../src/server/plugins/builtin/index.ts | 1 + .../website/src/components/agent-icons.tsx | 17 + .../website/src/components/landing-page.tsx | 10 +- .../website/src/components/site-footer.tsx | 6 + .../content/alternatives/claude-desktop.md | 42 +- .../src/content/alternatives/codex-app.md | 2 +- .../src/content/alternatives/conductor.md | 34 +- .../src/content/alternatives/happy-coder.md | 34 +- .../src/content/alternatives/openchamber.md | 36 +- .../content/alternatives/opencode-desktop.md | 38 +- .../website/src/content/alternatives/orca.md | 40 +- .../src/content/alternatives/superset.md | 2 +- packages/website/src/data/agent-pages.ts | 10 + packages/website/src/llms.ts | 2 +- packages/website/src/routeTree.gen.ts | 21 + packages/website/src/routes/agents.tsx | 2 +- packages/website/src/routes/antigravity.tsx | 4 + plugins/antigravity-provider/icon.svg | 3 + plugins/antigravity-provider/index.server.ts | 7 + .../antigravity-provider/paseo-plugin.json | 1 + .../server/internal/catalog.ts | 74 ++ .../server/internal/process.ts | 223 +++++ .../server/internal/prompt.ts | 64 ++ .../server/internal/session.ts | 397 +++++++++ .../server/internal/signals.ts | 40 + .../server/internal/timeline.ts | 55 ++ .../server/internal/wire.ts | 85 ++ .../server/provider.test.ts | 775 ++++++++++++++++++ .../antigravity-provider/server/provider.ts | 171 ++++ plugins/antigravity-provider/test/agy.mjs | 102 +++ .../test/fixtures/README.md | 100 +++ .../test/fixtures/agents.capture.ndjson | 2 + .../test/fixtures/agents.stderr.ndjson | 0 .../test/fixtures/agents.stdin.ndjson | 0 .../test/fixtures/agents.stdout.ndjson | 0 .../fixtures/api-key-env-only.capture.ndjson | 2 + .../fixtures/api-key-env-only.stderr.ndjson | 2 + .../fixtures/api-key-env-only.stdin.ndjson | 0 .../fixtures/api-key-env-only.stdout.ndjson | 1 + .../fixtures/api-key-settings.capture.ndjson | 2 + .../fixtures/api-key-settings.stderr.ndjson | 1 + .../fixtures/api-key-settings.stdin.ndjson | 1 + .../fixtures/api-key-settings.stdout.ndjson | 2 + .../cwd-without-add-dir.capture.ndjson | 2 + .../cwd-without-add-dir.stderr.ndjson | 0 .../fixtures/cwd-without-add-dir.stdin.ndjson | 1 + .../cwd-without-add-dir.stdout.ndjson | 11 + .../test/fixtures/denial-replay.stdout.ndjson | 11 + .../effort-encoded-match.capture.ndjson | 2 + .../effort-encoded-match.stderr.ndjson | 0 .../effort-encoded-match.stdin.ndjson | 1 + .../effort-encoded-match.stdout.ndjson | 19 + .../effort-encoded-mismatch.capture.ndjson | 3 + .../effort-encoded-mismatch.stderr.ndjson | 1 + .../effort-encoded-mismatch.stdin.ndjson | 0 .../effort-encoded-mismatch.stdout.ndjson | 1 + .../fixtures/effort-mismatch.capture.ndjson | 2 + .../fixtures/effort-mismatch.stderr.ndjson | 1 + .../fixtures/effort-mismatch.stdin.ndjson | 0 .../fixtures/effort-mismatch.stdout.ndjson | 1 + .../effort-unsupported-family.capture.ndjson | 2 + .../effort-unsupported-family.stderr.ndjson | 1 + .../effort-unsupported-family.stdin.ndjson | 0 .../effort-unsupported-family.stdout.ndjson | 1 + .../test/fixtures/help.capture.ndjson | 2 + .../test/fixtures/help.stderr.ndjson | 39 + .../test/fixtures/help.stdin.ndjson | 0 .../test/fixtures/help.stdout.ndjson | 0 .../fixtures/image-content.capture.ndjson | 2 + .../test/fixtures/image-content.stderr.ndjson | 1 + .../test/fixtures/image-content.stdin.ndjson | 1 + .../test/fixtures/image-content.stdout.ndjson | 2 + .../image-file-external.capture.ndjson | 2 + .../test/fixtures/image-file-external.png | Bin 0 -> 910 bytes .../image-file-external.stderr.ndjson | 0 .../fixtures/image-file-external.stdin.ndjson | 1 + .../image-file-external.stdout.ndjson | 14 + .../test/fixtures/mcp-add-http.capture.ndjson | 3 + .../test/fixtures/mcp-add-http.stderr.ndjson | 0 .../test/fixtures/mcp-add-http.stdin.ndjson | 0 .../test/fixtures/mcp-add-http.stdout.ndjson | 1 + .../test/fixtures/mcp-add.capture.ndjson | 2 + .../test/fixtures/mcp-add.stderr.ndjson | 1 + .../test/fixtures/mcp-add.stdin.ndjson | 0 .../test/fixtures/mcp-add.stdout.ndjson | 0 .../mcp-agent-definition.capture.ndjson | 2 + .../mcp-agent-definition.stderr.ndjson | 0 .../mcp-agent-definition.stdin.ndjson | 1 + .../mcp-agent-definition.stdout.ndjson | 15 + .../mcp-agent-discovered.capture.ndjson | 2 + .../mcp-agent-discovered.stderr.ndjson | 0 .../mcp-agent-discovered.stdin.ndjson | 1 + .../mcp-agent-discovered.stdout.ndjson | 19 + .../fixtures/mcp-agent-file.capture.ndjson | 2 + .../fixtures/mcp-agent-file.stderr.ndjson | 0 .../test/fixtures/mcp-agent-file.stdin.ndjson | 1 + .../fixtures/mcp-agent-file.stdout.ndjson | 39 + .../test/fixtures/mcp-global.capture.ndjson | 2 + .../test/fixtures/mcp-global.stderr.ndjson | 0 .../test/fixtures/mcp-global.stdin.ndjson | 1 + .../test/fixtures/mcp-global.stdout.ndjson | 14 + .../test/fixtures/mcp-help.capture.ndjson | 2 + .../test/fixtures/mcp-help.stderr.ndjson | 28 + .../test/fixtures/mcp-help.stdin.ndjson | 0 .../test/fixtures/mcp-help.stdout.ndjson | 0 .../test/fixtures/mcp-http.requests.ndjson | 20 + .../test/fixtures/mcp-list.capture.ndjson | 2 + .../test/fixtures/mcp-list.stderr.ndjson | 0 .../test/fixtures/mcp-list.stdin.ndjson | 0 .../test/fixtures/mcp-list.stdout.ndjson | 1 + .../fixtures/mcp-owned-cwd.capture.ndjson | 2 + .../test/fixtures/mcp-owned-cwd.stderr.ndjson | 0 .../test/fixtures/mcp-owned-cwd.stdin.ndjson | 1 + .../test/fixtures/mcp-owned-cwd.stdout.ndjson | 95 +++ .../mcp-owned-first-add-dir.capture.ndjson | 2 + .../mcp-owned-first-add-dir.stderr.ndjson | 0 .../mcp-owned-first-add-dir.stdin.ndjson | 1 + .../mcp-owned-first-add-dir.stdout.ndjson | 60 ++ .../mcp-project-config.capture.ndjson | 2 + .../fixtures/mcp-project-config.stderr.ndjson | 0 .../fixtures/mcp-project-config.stdin.ndjson | 1 + .../fixtures/mcp-project-config.stdout.ndjson | 5 + .../mcp-project-isolated.capture.ndjson | 4 + .../mcp-project-isolated.stderr.ndjson | 0 .../mcp-project-isolated.stdin.ndjson | 1 + .../mcp-project-isolated.stdout.ndjson | 31 + .../test/fixtures/mcp-project.capture.ndjson | 2 + .../test/fixtures/mcp-project.stderr.ndjson | 0 .../test/fixtures/mcp-project.stdin.ndjson | 1 + .../test/fixtures/mcp-project.stdout.ndjson | 15 + .../mcp-second-add-dir.capture.ndjson | 2 + .../fixtures/mcp-second-add-dir.stderr.ndjson | 0 .../fixtures/mcp-second-add-dir.stdin.ndjson | 1 + .../fixtures/mcp-second-add-dir.stdout.ndjson | 71 ++ .../model-switch-invalid.capture.ndjson | 3 + .../model-switch-invalid.stderr.ndjson | 13 + .../model-switch-invalid.stdin.ndjson | 0 .../model-switch-invalid.stdout.ndjson | 1 + .../model-switch-network.capture.ndjson | 3 + .../model-switch-network.stderr.ndjson | 0 .../model-switch-network.stdin.ndjson | 1 + .../model-switch-network.stdout.ndjson | 8 + .../model-switch-resume.capture.ndjson | 3 + .../model-switch-resume.stderr.ndjson | 13 + .../fixtures/model-switch-resume.stdin.ndjson | 0 .../model-switch-resume.stdout.ndjson | 1 + .../model-switch-valid.capture.ndjson | 2 + .../fixtures/model-switch-valid.stderr.ndjson | 0 .../fixtures/model-switch-valid.stdin.ndjson | 1 + .../fixtures/model-switch-valid.stdout.ndjson | 7 + .../fixtures/models-api-key.capture.ndjson | 2 + .../fixtures/models-api-key.stderr.ndjson | 1 + .../test/fixtures/models-api-key.stdin.ndjson | 0 .../fixtures/models-api-key.stdout.ndjson | 11 + .../models-unauthenticated.capture.ndjson | 2 + .../models-unauthenticated.stderr.ndjson | 2 + .../models-unauthenticated.stdin.ndjson | 0 .../models-unauthenticated.stdout.ndjson | 0 .../multi-turn-reasoning.capture.ndjson | 2 + .../multi-turn-reasoning.stderr.ndjson | 0 .../multi-turn-reasoning.stdin.ndjson | 2 + .../multi-turn-reasoning.stdout.ndjson | 51 ++ .../fixtures/not-signed-in.capture.ndjson | 2 + .../test/fixtures/not-signed-in.stderr.ndjson | 2 + .../test/fixtures/not-signed-in.stdin.ndjson | 0 .../test/fixtures/not-signed-in.stdout.ndjson | 1 + .../test/fixtures/parity-inspection.ndjson | 10 + .../fixtures/project-skill.capture.ndjson | 2 + .../test/fixtures/project-skill.stderr.ndjson | 0 .../test/fixtures/project-skill.stdin.ndjson | 1 + .../test/fixtures/project-skill.stdout.ndjson | 9 + .../question-auto-denial.capture.ndjson | 2 + .../question-auto-denial.stderr.ndjson | 0 .../question-auto-denial.stdin.ndjson | 1 + .../question-auto-denial.stdout.ndjson | 14 + .../resume-after-interrupt.capture.ndjson | 2 + .../resume-after-interrupt.stderr.ndjson | 0 .../resume-after-interrupt.stdin.ndjson | 1 + .../resume-after-interrupt.stdout.ndjson | 7 + .../test/fixtures/resume.capture.ndjson | 2 + .../test/fixtures/resume.stderr.ndjson | 0 .../test/fixtures/resume.stdin.ndjson | 1 + .../test/fixtures/resume.stdout.ndjson | 8 + .../fixtures/schema-prompt.capture.ndjson | 3 + .../test/fixtures/schema-prompt.stderr.ndjson | 1 + .../test/fixtures/schema-prompt.stdin.ndjson | 1 + .../test/fixtures/schema-prompt.stdout.ndjson | 1 + .../schema-user-message.capture.ndjson | 2 + .../schema-user-message.stderr.ndjson | 1 + .../fixtures/schema-user-message.stdin.ndjson | 1 + .../schema-user-message.stdout.ndjson | 2 + .../test/fixtures/schema-user.capture.ndjson | 2 + .../test/fixtures/schema-user.stderr.ndjson | 1 + .../test/fixtures/schema-user.stdin.ndjson | 1 + .../test/fixtures/schema-user.stdout.ndjson | 2 + .../fixtures/sigint-mid-text.capture.ndjson | 3 + .../fixtures/sigint-mid-text.stderr.ndjson | 1 + .../fixtures/sigint-mid-text.stdin.ndjson | 1 + .../fixtures/sigint-mid-text.stdout.ndjson | 4 + .../fixtures/sigint-mid-tool.capture.ndjson | 3 + .../fixtures/sigint-mid-tool.stderr.ndjson | 1 + .../fixtures/sigint-mid-tool.stdin.ndjson | 1 + .../fixtures/sigint-mid-tool.stdout.ndjson | 5 + .../test/fixtures/sigterm.capture.ndjson | 3 + .../test/fixtures/sigterm.stderr.ndjson | 1 + .../test/fixtures/sigterm.stdin.ndjson | 1 + .../test/fixtures/sigterm.stdout.ndjson | 3 + .../fixtures/slash-disabled.capture.ndjson | 2 + .../fixtures/slash-disabled.stderr.ndjson | 0 .../test/fixtures/slash-disabled.stdin.ndjson | 1 + .../fixtures/slash-disabled.stdout.ndjson | 84 ++ .../fixtures/slash-enabled.capture.ndjson | 2 + .../test/fixtures/slash-enabled.stderr.ndjson | 1 + .../test/fixtures/slash-enabled.stdin.ndjson | 1 + .../test/fixtures/slash-enabled.stdout.ndjson | 2 + .../test/fixtures/state-inspection.ndjson | 6 + .../stdin-close-mid-turn.capture.ndjson | 3 + .../stdin-close-mid-turn.stderr.ndjson | 0 .../stdin-close-mid-turn.stdin.ndjson | 1 + .../stdin-close-mid-turn.stdout.ndjson | 5 + .../fixtures/stdin-mid-turn.capture.ndjson | 2 + .../fixtures/stdin-mid-turn.stderr.ndjson | 0 .../test/fixtures/stdin-mid-turn.stdin.ndjson | 2 + .../fixtures/stdin-mid-turn.stdout.ndjson | 179 ++++ .../test/fixtures/subagent.capture.ndjson | 2 + .../test/fixtures/subagent.stderr.ndjson | 0 .../test/fixtures/subagent.stdin.ndjson | 1 + .../test/fixtures/subagent.stdout.ndjson | 23 + .../test/fixtures/text-block.capture.ndjson | 2 + .../test/fixtures/text-block.stderr.ndjson | 0 .../test/fixtures/text-block.stdin.ndjson | 1 + .../test/fixtures/text-block.stdout.ndjson | 5 + .../test/fixtures/text-turn.capture.ndjson | 2 + .../test/fixtures/text-turn.stderr.ndjson | 0 .../test/fixtures/text-turn.stdin.ndjson | 1 + .../test/fixtures/text-turn.stdout.ndjson | 5 + .../tools-accept-edits.capture.ndjson | 2 + .../fixtures/tools-accept-edits.stderr.ndjson | 1 + .../fixtures/tools-accept-edits.stdin.ndjson | 3 + .../fixtures/tools-accept-edits.stdout.ndjson | 40 + .../fixtures/tools-default.capture.ndjson | 2 + .../test/fixtures/tools-default.stderr.ndjson | 1 + .../test/fixtures/tools-default.stdin.ndjson | 3 + .../test/fixtures/tools-default.stdout.ndjson | 41 + .../test/fixtures/tools-plan.capture.ndjson | 2 + .../test/fixtures/tools-plan.stderr.ndjson | 4 + .../test/fixtures/tools-plan.stdin.ndjson | 3 + .../test/fixtures/tools-plan.stdout.ndjson | 36 + .../fixtures/tools-sandbox.capture.ndjson | 2 + .../test/fixtures/tools-sandbox.stderr.ndjson | 1 + .../test/fixtures/tools-sandbox.stdin.ndjson | 3 + .../test/fixtures/tools-sandbox.stdout.ndjson | 40 + .../tools-skip-permissions.capture.ndjson | 2 + .../tools-skip-permissions.stderr.ndjson | 0 .../tools-skip-permissions.stdin.ndjson | 3 + .../tools-skip-permissions.stdout.ndjson | 42 + .../test/fixtures/version.capture.ndjson | 2 + .../test/fixtures/version.stderr.ndjson | 0 .../test/fixtures/version.stdin.ndjson | 0 .../test/fixtures/version.stdout.ndjson | 1 + public-docs/providers.md | 4 +- public-docs/supported-providers.md | 38 + 268 files changed, 3854 insertions(+), 127 deletions(-) create mode 100644 packages/website/src/routes/antigravity.tsx create mode 100644 plugins/antigravity-provider/icon.svg create mode 100644 plugins/antigravity-provider/index.server.ts create mode 100644 plugins/antigravity-provider/paseo-plugin.json create mode 100644 plugins/antigravity-provider/server/internal/catalog.ts create mode 100644 plugins/antigravity-provider/server/internal/process.ts create mode 100644 plugins/antigravity-provider/server/internal/prompt.ts create mode 100644 plugins/antigravity-provider/server/internal/session.ts create mode 100644 plugins/antigravity-provider/server/internal/signals.ts create mode 100644 plugins/antigravity-provider/server/internal/timeline.ts create mode 100644 plugins/antigravity-provider/server/internal/wire.ts create mode 100644 plugins/antigravity-provider/server/provider.test.ts create mode 100644 plugins/antigravity-provider/server/provider.ts create mode 100644 plugins/antigravity-provider/test/agy.mjs create mode 100644 plugins/antigravity-provider/test/fixtures/README.md create mode 100644 plugins/antigravity-provider/test/fixtures/agents.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/agents.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/agents.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/agents.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/api-key-env-only.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/api-key-env-only.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/api-key-env-only.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/api-key-env-only.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/api-key-settings.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/api-key-settings.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/api-key-settings.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/api-key-settings.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/cwd-without-add-dir.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/cwd-without-add-dir.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/cwd-without-add-dir.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/cwd-without-add-dir.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/denial-replay.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/effort-encoded-match.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/effort-encoded-match.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/effort-encoded-match.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/effort-encoded-match.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/effort-encoded-mismatch.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/effort-encoded-mismatch.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/effort-encoded-mismatch.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/effort-encoded-mismatch.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/effort-mismatch.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/effort-mismatch.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/effort-mismatch.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/effort-mismatch.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/effort-unsupported-family.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/effort-unsupported-family.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/effort-unsupported-family.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/effort-unsupported-family.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/help.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/help.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/help.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/help.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/image-content.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/image-content.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/image-content.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/image-content.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/image-file-external.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/image-file-external.png create mode 100644 plugins/antigravity-provider/test/fixtures/image-file-external.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/image-file-external.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/image-file-external.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-add-http.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-add-http.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-add-http.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-add-http.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-add.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-add.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-add.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-add.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-agent-definition.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-agent-definition.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-agent-definition.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-agent-definition.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-agent-discovered.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-agent-discovered.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-agent-discovered.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-agent-discovered.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-agent-file.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-agent-file.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-agent-file.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-agent-file.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-global.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-global.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-global.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-global.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-help.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-help.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-help.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-help.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-http.requests.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-list.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-list.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-list.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-list.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-owned-cwd.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-owned-cwd.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-owned-cwd.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-owned-cwd.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-owned-first-add-dir.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-owned-first-add-dir.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-owned-first-add-dir.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-owned-first-add-dir.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-project-config.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-project-config.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-project-config.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-project-config.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-project-isolated.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-project-isolated.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-project-isolated.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-project-isolated.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-project.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-project.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-project.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-project.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-second-add-dir.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-second-add-dir.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-second-add-dir.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/mcp-second-add-dir.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/model-switch-invalid.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/model-switch-invalid.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/model-switch-invalid.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/model-switch-invalid.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/model-switch-network.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/model-switch-network.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/model-switch-network.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/model-switch-network.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/model-switch-resume.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/model-switch-resume.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/model-switch-resume.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/model-switch-resume.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/model-switch-valid.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/model-switch-valid.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/model-switch-valid.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/model-switch-valid.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/models-api-key.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/models-api-key.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/models-api-key.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/models-api-key.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/models-unauthenticated.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/models-unauthenticated.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/models-unauthenticated.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/models-unauthenticated.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/multi-turn-reasoning.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/multi-turn-reasoning.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/multi-turn-reasoning.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/multi-turn-reasoning.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/not-signed-in.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/not-signed-in.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/not-signed-in.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/not-signed-in.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/parity-inspection.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/project-skill.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/project-skill.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/project-skill.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/project-skill.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/question-auto-denial.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/question-auto-denial.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/question-auto-denial.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/question-auto-denial.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/resume-after-interrupt.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/resume-after-interrupt.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/resume-after-interrupt.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/resume-after-interrupt.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/resume.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/resume.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/resume.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/resume.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/schema-prompt.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/schema-prompt.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/schema-prompt.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/schema-prompt.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/schema-user-message.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/schema-user-message.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/schema-user-message.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/schema-user-message.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/schema-user.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/schema-user.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/schema-user.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/schema-user.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/sigint-mid-text.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/sigint-mid-text.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/sigint-mid-text.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/sigint-mid-text.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/sigint-mid-tool.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/sigint-mid-tool.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/sigint-mid-tool.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/sigint-mid-tool.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/sigterm.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/sigterm.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/sigterm.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/sigterm.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/slash-disabled.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/slash-disabled.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/slash-disabled.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/slash-disabled.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/slash-enabled.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/slash-enabled.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/slash-enabled.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/slash-enabled.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/state-inspection.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/stdin-close-mid-turn.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/stdin-close-mid-turn.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/stdin-close-mid-turn.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/stdin-close-mid-turn.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/stdin-mid-turn.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/stdin-mid-turn.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/stdin-mid-turn.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/stdin-mid-turn.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/subagent.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/subagent.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/subagent.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/subagent.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/text-block.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/text-block.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/text-block.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/text-block.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/text-turn.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/text-turn.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/text-turn.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/text-turn.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/tools-accept-edits.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/tools-accept-edits.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/tools-accept-edits.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/tools-accept-edits.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/tools-default.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/tools-default.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/tools-default.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/tools-default.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/tools-plan.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/tools-plan.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/tools-plan.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/tools-plan.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/tools-sandbox.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/tools-sandbox.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/tools-sandbox.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/tools-sandbox.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/tools-skip-permissions.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/tools-skip-permissions.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/tools-skip-permissions.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/tools-skip-permissions.stdout.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/version.capture.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/version.stderr.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/version.stdin.ndjson create mode 100644 plugins/antigravity-provider/test/fixtures/version.stdout.ndjson diff --git a/CLAUDE.md b/CLAUDE.md index 80aabf3910..e1ac653338 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,7 +2,7 @@ Paseo is a mobile app for monitoring and controlling your local AI coding agents from anywhere. Your dev environment, in your pocket. Connects directly to your actual development environment — your code stays on your machine. -**Supported agents:** Claude Code, Codex, GitHub Copilot, OpenCode, Pi, and Muse Code. +**Supported agents:** Claude Code, Codex, GitHub Copilot, OpenCode, Pi, Antigravity, and Muse Code. ## Repository map diff --git a/README.md b/README.md index 629477a3ad..aa0a841e7e 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@

-

One interface for Claude Code, Codex, Copilot, OpenCode, Pi, and Muse Code agents.

+

One interface for Claude Code, Codex, Copilot, OpenCode, Pi, Antigravity, and Muse Code agents.

Paseo app screenshot @@ -42,7 +42,7 @@ Run agents in parallel on your own machines. Ship from your phone or your desk. - **Self-hosted:** Agents run on your machine with your full dev environment. Use your tools, your configs, and your skills. -- **Multi-provider:** Claude Code, Codex, Copilot, OpenCode, Pi, and Muse Code through the same interface. Pick the right model for each job. +- **Multi-provider:** Claude Code, Codex, Copilot, OpenCode, Pi, Antigravity, and Muse Code through the same interface. Pick the right model for each job. - **Voice control:** Dictate tasks or talk through problems in voice mode. Hands-free when you need it. - **Cross-device:** iOS, Android, desktop, web, and CLI. Start work at your desk, check in from your phone, script it from the terminal. - **Privacy-first:** Paseo doesn't have any telemetry, tracking, or forced log-ins. @@ -68,6 +68,7 @@ You need at least one agent CLI installed and configured with your credentials: - [GitHub Copilot](https://github.com/features/copilot/cli/) - [OpenCode](https://github.com/anomalyco/opencode) - [Pi](https://pi.dev) +- [Antigravity](https://paseo.sh/docs/supported-providers#antigravity) - [Muse Code](https://paseo.sh/docs/muse-code) ### Desktop app (recommended) diff --git a/docs/providers.md b/docs/providers.md index 0558406a1b..0036c2e6f2 100644 --- a/docs/providers.md +++ b/docs/providers.md @@ -6,7 +6,7 @@ server integrations. ## Plugin providers Keep a bundled provider in `plugins//` and register it through -`@getpaseo/plugin/server/provider`. Muse Code follows this pattern. Built-in loading and SDK +`@getpaseo/plugin/server/provider`. Antigravity and Muse Code follow this pattern. Built-in loading and SDK import rules belong to [plugins.md](plugins.md#built-in-plugins); the [public provider guide](../public-docs/plugins/providers.md) covers the provider contract. @@ -15,9 +15,10 @@ executable resolution and applies `agents.providers..command` and ` connecting. Register the provider's icon with the plugin rather than adding it to the app's provider icon map. You do not need a core manifest entry or provider factory. -| Provider | Transport | Setup and limitations | -| --------- | ------------------------------------------ | ---------------------------------------- | -| Muse Code | MSP over one `muse serve` host per session | [Muse Code](../public-docs/muse-code.md) | +| Provider | Transport | Setup and limitations | +| ----------- | ------------------------------------------ | ---------------------------------------------------------------- | +| Antigravity | Installed `agy` CLI | [Antigravity](../public-docs/supported-providers.md#antigravity) | +| Muse Code | MSP over one `muse serve` host per session | [Muse Code](../public-docs/muse-code.md) | ## Provider-native session options diff --git a/packages/server/src/server/agent/plugin-provider.test.ts b/packages/server/src/server/agent/plugin-provider.test.ts index da416ee7e1..8b894a7765 100644 --- a/packages/server/src/server/agent/plugin-provider.test.ts +++ b/packages/server/src/server/agent/plugin-provider.test.ts @@ -794,6 +794,56 @@ async function withObservedProvider( } } +test("commits notices emitted during session open as the initial timeline with stable history timestamps", async () => { + await withObservedProvider( + { + openChildren(sessionId, emit) { + emit({ + type: "session.notice", + sessionId, + notice: { + id: "startup", + severity: "warning", + title: "Full access", + description: "Tools run without asking.", + }, + }); + emit({ + type: "session.notice", + sessionId, + notice: { id: "dismissed", severity: "info", title: "Hidden", dismissed: true }, + }); + }, + }, + async (client) => { + const session = await client.createSession({ provider: client.provider, cwd: "/workspace" }); + try { + expect(session.initialTimeline).toEqual([ + { + timestamp: expect.any(String), + item: { + type: "notification", + level: "warning", + message: "Full access\nTools run without asking.", + }, + }, + ]); + const history: AgentStreamEvent[] = []; + for await (const event of session.streamHistory()) history.push(event); + expect( + history.filter( + (event) => event.type === "timeline" && event.item.type === "notification", + ), + ).toEqual([ + { type: "timeline", provider: client.provider, ...session.initialTimeline![0] }, + ]); + } finally { + await session.close(); + } + }, + ); +}); + describe("pending provider responses", () => { test.each(["session.open", "catalog", "session.configure", "session.prompt"])( "preserves the send error for %s", diff --git a/packages/server/src/server/agent/plugin-provider.ts b/packages/server/src/server/agent/plugin-provider.ts index a17a8f7601..58bddd07b6 100644 --- a/packages/server/src/server/agent/plugin-provider.ts +++ b/packages/server/src/server/agent/plugin-provider.ts @@ -1090,6 +1090,7 @@ class PluginAgentClient implements AgentClient { } class PluginAgentSession implements AgentSession { + readonly initialTimeline: ImportedProviderSession["timeline"]; private readonly listeners = new Set<(event: AgentStreamEvent) => void>(); private readonly history: AgentStreamEvent[] = []; private readonly pendingPermissions = new Map(); @@ -1110,6 +1111,11 @@ class PluginAgentSession implements AgentSession { ) { this.subagentIdsBySession.set(bridge.id, null); for (const event of bridge.history) this.accept(event, false); + // Opening notices arrive before the manager subscribes to the session. + // Commit them on registration and replay the same timestamps from history. + this.initialTimeline = this.timelineHistory().filter( + (entry) => entry.item.type === "notification", + ); this.unsubscribe = bridge.onEvent((event) => this.accept(event, true)); } @@ -1490,6 +1496,7 @@ class PluginAgentSession implements AgentSession { { type: "timeline", provider: this.provider, + timestamp: new Date().toISOString(), item: { type: "notification", level: event.notice.severity, diff --git a/packages/server/src/server/daemon-e2e/models.e2e.test.ts b/packages/server/src/server/daemon-e2e/models.e2e.test.ts index 7691e63713..1f2086c879 100644 --- a/packages/server/src/server/daemon-e2e/models.e2e.test.ts +++ b/packages/server/src/server/daemon-e2e/models.e2e.test.ts @@ -1,4 +1,11 @@ import { describe, test, expect } from "vitest"; +import { mkdtemp, rm } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { findExecutable } from "../../executable-resolution/executable-resolution.js"; +import { BuiltinPluginLoader } from "../plugins/builtin/index.js"; +import { createTestPaseoDaemon } from "../test-utils/paseo-daemon.js"; +import { DaemonClient } from "../test-utils/daemon-client.js"; import { execFileSync } from "node:child_process"; import { createDaemonTestContext } from "../test-utils/index.js"; @@ -96,3 +103,65 @@ describe("daemon E2E", () => { ); }); }); + +test("Antigravity creates an agent and answers a prompt with real agy", async ({ skip }) => { + const command = await findExecutable(process.env.AGY_COMMAND ?? "agy"); + if (command === null) { + skip("agy is not resolvable"); + return; + } + const cwd = await mkdtemp(path.join(os.tmpdir(), "paseo-antigravity-e2e-")); + const daemon = await createTestPaseoDaemon({ + agentClients: {}, + mcpEnabled: false, + builtinPlugins: new BuiltinPluginLoader(undefined, ["antigravity-provider"]), + providerOverrides: { antigravity: { command: [command], paseoTools: { enabled: false } } }, + }); + const client = new DaemonClient({ + url: `ws://127.0.0.1:${daemon.port}/ws`, + appVersion: "0.10.0", + }); + try { + await client.connect(); + await client.fetchAgents({ subscribe: {} }); + await expect + .poll( + async () => { + const snapshot = await client.getProvidersSnapshot({ cwd }); + return snapshot.entries.find((entry) => entry.provider === "antigravity")?.status; + }, + { timeout: 30000 }, + ) + .toBe("ready"); + const agent = await client.createAgent({ + provider: "antigravity", + cwd, + title: "Antigravity E2E", + }); + expect(agent.currentModeId).toBe("full-access"); + expect(agent.availableModes?.map((mode) => mode.id)).toEqual(["full-access"]); + const openedTimeline = await client.fetchAgentTimeline(agent.id, { limit: 100 }); + expect(openedTimeline.entries.map((entry) => entry.item)).toEqual([ + { + type: "notification", + level: "warning", + message: + "Antigravity is running with full access\nAntigravity's CLI cannot ask for permission when another app drives it, so Paseo starts it with --dangerously-skip-permissions. Every tool call, including shell commands, runs without asking.", + }, + ]); + await client.sendMessage(agent.id, "Reply with exactly ANTIGRAVITY_E2E_OK. No tools."); + const result = await client.waitForFinish(agent.id, 90000); + expect(result.status).toBe("idle"); + const timeline = await client.fetchAgentTimeline(agent.id, { limit: 100 }); + expect( + timeline.entries.some( + (entry) => + entry.item.type === "assistant_message" && entry.item.text.includes("ANTIGRAVITY_E2E_OK"), + ), + ).toBe(true); + } finally { + await client.close(); + await daemon.close(); + await rm(cwd, { recursive: true, force: true }); + } +}, 120000); diff --git a/packages/server/src/server/plugins/builtin/index.ts b/packages/server/src/server/plugins/builtin/index.ts index 179768dd2f..eb6aab147e 100644 --- a/packages/server/src/server/plugins/builtin/index.ts +++ b/packages/server/src/server/plugins/builtin/index.ts @@ -3,6 +3,7 @@ import path from "node:path"; import { fileURLToPath } from "node:url"; export const builtinPlugins = [ + "antigravity-provider", "claude-usage-source", "codex-usage-source", "copilot-usage-source", diff --git a/packages/website/src/components/agent-icons.tsx b/packages/website/src/components/agent-icons.tsx index e827d1e484..8488050b12 100644 --- a/packages/website/src/components/agent-icons.tsx +++ b/packages/website/src/components/agent-icons.tsx @@ -99,3 +99,20 @@ export function MuseCodeIcon(props: React.SVGProps) { ); } + +export function AntigravityIcon(props: React.SVGProps) { + return ( + + ); +} diff --git a/packages/website/src/components/landing-page.tsx b/packages/website/src/components/landing-page.tsx index db25dbe1ea..6a29c62235 100644 --- a/packages/website/src/components/landing-page.tsx +++ b/packages/website/src/components/landing-page.tsx @@ -79,6 +79,7 @@ import { OpenCodeIcon, PiIcon, MuseCodeIcon, + AntigravityIcon, } from "~/components/agent-icons"; import { DiscordIcon, GitHubIcon, SlackIcon } from "~/components/brand-icons"; import { ClaudeIcon, MobileChat, MobileDiff, MobileSidebar, PhoneFrame } from "~/components/mockup"; @@ -162,6 +163,7 @@ function Hero({ title, subtitle }: { title: React.ReactNode; subtitle: React.Rea const CLAUDE_CODE_BADGE_ICON = ; const CODEX_BADGE_ICON = ; +const ANTIGRAVITY_BADGE_ICON = ; const MUSE_CODE_BADGE_ICON = ; const OPENCODE_BADGE_ICON = ; const PI_BADGE_ICON = ; @@ -434,6 +436,7 @@ function MultiProviderSection() { { name: "Claude Code", icon: }, { name: "Codex", icon: }, { name: "OpenCode", icon: }, + { name: "Antigravity", icon: }, { name: "Muse Code", icon: }, { name: "Pi", icon: }, { name: "Cursor", icon: }, @@ -903,7 +906,8 @@ const PRINCIPLES: ReadonlyArray<{ icon: LucideIcon; title: string; description: { icon: Merge, title: "Unified", - description: "Claude Code, Codex, OpenCode, Pi, Muse Code, and more, all used the same way.", + description: + "Claude Code, Codex, OpenCode, Pi, Antigravity, Muse Code, and more, all used the same way.", }, { icon: Blocks, @@ -965,6 +969,7 @@ function GetStarted() { + @@ -1211,7 +1216,8 @@ function FAQ() { Paseo supports many providers. It has custom implementations for Claude, Codex, OpenCode, - Pi, OMP, and Muse Code, and supports many more via ACP. See the full list here:{" "} + Pi, OMP, Antigravity, and Muse Code, and supports many more via ACP. See the full list + here:{" "} all supported providers diff --git a/packages/website/src/components/site-footer.tsx b/packages/website/src/components/site-footer.tsx index 9009e63e53..ba092410da 100644 --- a/packages/website/src/components/site-footer.tsx +++ b/packages/website/src/components/site-footer.tsx @@ -86,6 +86,12 @@ export function SiteFooter({ width = "default" }: SiteFooterProps) { > OpenCode + + Antigravity + rootRouteImport, } as any); +const AntigravityRoute = AntigravityRouteImport.update({ + id: "/antigravity", + path: "/antigravity", + getParentRoute: () => rootRouteImport, +} as any); const AmpRoute = AmpRouteImport.update({ id: "/amp", path: "/amp", @@ -390,6 +396,7 @@ export interface FileRoutesByFullPath { "/agents": typeof AgentsRoute; "/agoragentic": typeof AgoragenticRoute; "/amp": typeof AmpRoute; + "/antigravity": typeof AntigravityRoute; "/auggie": typeof AuggieRoute; "/autohand": typeof AutohandRoute; "/blog": typeof BlogRouteWithChildren; @@ -454,6 +461,7 @@ export interface FileRoutesByTo { "/agents": typeof AgentsRoute; "/agoragentic": typeof AgoragenticRoute; "/amp": typeof AmpRoute; + "/antigravity": typeof AntigravityRoute; "/auggie": typeof AuggieRoute; "/autohand": typeof AutohandRoute; "/changelog": typeof ChangelogRoute; @@ -517,6 +525,7 @@ export interface FileRoutesById { "/agents": typeof AgentsRoute; "/agoragentic": typeof AgoragenticRoute; "/amp": typeof AmpRoute; + "/antigravity": typeof AntigravityRoute; "/auggie": typeof AuggieRoute; "/autohand": typeof AutohandRoute; "/blog": typeof BlogRouteWithChildren; @@ -583,6 +592,7 @@ export interface FileRouteTypes { | "/agents" | "/agoragentic" | "/amp" + | "/antigravity" | "/auggie" | "/autohand" | "/blog" @@ -647,6 +657,7 @@ export interface FileRouteTypes { | "/agents" | "/agoragentic" | "/amp" + | "/antigravity" | "/auggie" | "/autohand" | "/changelog" @@ -709,6 +720,7 @@ export interface FileRouteTypes { | "/agents" | "/agoragentic" | "/amp" + | "/antigravity" | "/auggie" | "/autohand" | "/blog" @@ -774,6 +786,7 @@ export interface RootRouteChildren { AgentsRoute: typeof AgentsRoute; AgoragenticRoute: typeof AgoragenticRoute; AmpRoute: typeof AmpRoute; + AntigravityRoute: typeof AntigravityRoute; AuggieRoute: typeof AuggieRoute; AutohandRoute: typeof AutohandRoute; BlogRoute: typeof BlogRouteWithChildren; @@ -1154,6 +1167,13 @@ declare module "@tanstack/react-router" { preLoaderRoute: typeof AuggieRouteImport; parentRoute: typeof rootRouteImport; }; + "/antigravity": { + id: "/antigravity"; + path: "/antigravity"; + fullPath: "/antigravity"; + preLoaderRoute: typeof AntigravityRouteImport; + parentRoute: typeof rootRouteImport; + }; "/amp": { id: "/amp"; path: "/amp"; @@ -1298,6 +1318,7 @@ const rootRouteChildren: RootRouteChildren = { AgentsRoute: AgentsRoute, AgoragenticRoute: AgoragenticRoute, AmpRoute: AmpRoute, + AntigravityRoute: AntigravityRoute, AuggieRoute: AuggieRoute, AutohandRoute: AutohandRoute, BlogRoute: BlogRouteWithChildren, diff --git a/packages/website/src/routes/agents.tsx b/packages/website/src/routes/agents.tsx index 551eddc94b..03548fe3bf 100644 --- a/packages/website/src/routes/agents.tsx +++ b/packages/website/src/routes/agents.tsx @@ -10,7 +10,7 @@ export const Route = createFileRoute("/agents")({ head: () => pageMeta( "Supported agents – Every coding agent Paseo runs", - "Run Claude Code, Codex, Copilot, OpenCode, Muse Code, Cursor CLI, Gemini CLI, and dozens more coding agents from your phone. Self-hosted, your code stays on your machine.", + "Run Claude Code, Codex, Copilot, OpenCode, Antigravity, Muse Code, Cursor CLI, Gemini CLI, and dozens more coding agents from your phone. Self-hosted, your code stays on your machine.", "/agents", ), component: AgentsPage, diff --git a/packages/website/src/routes/antigravity.tsx b/packages/website/src/routes/antigravity.tsx new file mode 100644 index 0000000000..3d821de7b9 --- /dev/null +++ b/packages/website/src/routes/antigravity.tsx @@ -0,0 +1,4 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { agentRouteOptions } from "~/components/agent-route"; + +export const Route = createFileRoute("/antigravity")(agentRouteOptions("antigravity")); diff --git a/plugins/antigravity-provider/icon.svg b/plugins/antigravity-provider/icon.svg new file mode 100644 index 0000000000..0576d92a87 --- /dev/null +++ b/plugins/antigravity-provider/icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/plugins/antigravity-provider/index.server.ts b/plugins/antigravity-provider/index.server.ts new file mode 100644 index 0000000000..661fa8445d --- /dev/null +++ b/plugins/antigravity-provider/index.server.ts @@ -0,0 +1,7 @@ +import type { PluginServerContext } from "@getpaseo/plugin/server"; +import { createAntigravityProvider } from "./server/provider.js"; + +export default function contribute(server: PluginServerContext) { + server.registerProvider(createAntigravityProvider()); + return () => {}; +} diff --git a/plugins/antigravity-provider/paseo-plugin.json b/plugins/antigravity-provider/paseo-plugin.json new file mode 100644 index 0000000000..ce6b2834f2 --- /dev/null +++ b/plugins/antigravity-provider/paseo-plugin.json @@ -0,0 +1 @@ +{ "id": "antigravity-provider", "requirements": { "paseo": ">=0.10.0" } } diff --git a/plugins/antigravity-provider/server/internal/catalog.ts b/plugins/antigravity-provider/server/internal/catalog.ts new file mode 100644 index 0000000000..b68c87f178 --- /dev/null +++ b/plugins/antigravity-provider/server/internal/catalog.ts @@ -0,0 +1,74 @@ +import type { + ProviderCatalog, + ProviderLaunch, + ProviderModel, + ProviderMode, + ProviderStatus, +} from "@getpaseo/plugin/server/provider"; +import { probe } from "./process.js"; +import { AntigravityError } from "./wire.js"; + +const modes: readonly ProviderMode[] = [ + { + id: "full-access", + icon: "ShieldOff", + colorTier: "dangerous", + label: "Full access", + description: + "Antigravity cannot ask for permission when another app drives it. Paseo starts it with --dangerously-skip-permissions.", + isUnattended: true, + }, +]; + +export async function getCatalog(launch: ProviderLaunch, cwd?: string): Promise { + const output = await probe({ launch, args: ["models"], cwd }); + const models: ProviderModel[] = []; + for (const line of output.trim().split(/\r?\n/)) { + const [id, label, ...extra] = line.split("\t"); + if (!id || !label || extra.length > 0) + throw new AntigravityError("Invalid `agy models` output", "INVALID_CATALOG"); + if (models.some((model) => model.id === id)) + throw new AntigravityError(`Duplicate Antigravity model: ${id}`, "INVALID_CATALOG"); + models.push({ id, label }); + } + return { models, modes, thinkingOptions: [], defaultMode: "full-access" }; +} + +export async function getStatus(launch: ProviderLaunch): Promise { + try { + const version = (await probe({ launch, args: ["--version"] })).trim(); + const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(version); + if (!match) + return { + available: false, + diagnostic: "Could not read the Antigravity version. Update `agy` to 1.1.15 or later.", + }; + const [, major, minor, patch] = match.map(Number); + const supported = major > 1 || (major === 1 && (minor > 1 || (minor === 1 && patch >= 15))); + if (!supported) + return { + available: false, + diagnostic: `Antigravity ${version} is unsupported. Update \`agy\` to 1.1.15 or later.`, + }; + await getCatalog(launch); + return { available: true }; + } catch (error) { + if (!(error instanceof AntigravityError)) throw error; + return { available: false, diagnostic: error.message }; + } +} + +export function validateSelection( + config: { model?: string; mode?: string; thinkingOption?: string }, + catalog: ProviderCatalog, +): void { + if (config.model && !catalog.models.some((model) => model.id === config.model)) + throw new AntigravityError(`Unknown Antigravity model: ${config.model}`, "INVALID_MODEL"); + if (config.mode && !catalog.modes.some((mode) => mode.id === config.mode)) + throw new AntigravityError(`Unknown Antigravity mode: ${config.mode}`, "INVALID_MODE"); + if (config.thinkingOption) + throw new AntigravityError( + "Antigravity effort is selected through the model ID", + "UNSUPPORTED_THINKING", + ); +} diff --git a/plugins/antigravity-provider/server/internal/process.ts b/plugins/antigravity-provider/server/internal/process.ts new file mode 100644 index 0000000000..899a88e5cd --- /dev/null +++ b/plugins/antigravity-provider/server/internal/process.ts @@ -0,0 +1,223 @@ +import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process"; +import { signalProcess } from "./signals.js"; +import { createInterface } from "node:readline"; +import type { ProviderLaunch, ProviderSessionConfig } from "@getpaseo/plugin/server/provider"; +import { + AntigravityError, + decodeFrame, + diagnostic, + encodePrompt, + type Frame, + type Init, +} from "./wire.js"; + +interface DriverOptions { + launch: ProviderLaunch; + config: ProviderSessionConfig; + conversationId: string | null; + onFrame(frame: Frame): void; + onExit(error: AntigravityError): void; +} + +export interface Driver { + ready: Promise; + selection: string; + prompt(text: string): Promise; + stop(reason: "interrupt" | "close"): Promise; +} + +export function selection(config: ProviderSessionConfig): string { + return JSON.stringify([config.model, config.mode]); +} + +function driverArgs(options: DriverOptions): string[] { + const { config, conversationId } = options; + const args = [ + ...options.launch.args, + "--input-format", + "stream-json", + "--output-format", + "stream-json", + "--add-dir", + config.cwd, + "--disable-slash-commands", + "--dangerously-skip-permissions", + ]; + if (config.model) args.push("--model", config.model); + if (conversationId) args.push("--conversation", conversationId); + return args; +} + +export function startDriver(options: DriverOptions): Driver { + const child = spawn(options.launch.command, driverArgs(options), { + cwd: options.config.cwd, + env: { ...options.launch.env, ...options.config.env }, + detached: process.platform !== "win32", + stdio: "pipe", + }); + const lines = createInterface({ input: child.stdout }); + let cleanup = Promise.resolve(); + let stderr = ""; + let state: "starting" | "ready" | "stopping" | "exited" = "starting"; + let resolveReady: (init: Init) => void; + let rejectReady: (error: Error) => void; + const ready = new Promise((resolve, reject) => { + resolveReady = resolve; + rejectReady = reject; + }); + let resolveExit: () => void; + const exited = new Promise((resolve) => { + resolveExit = resolve; + }); + const startupDeadline = setTimeout( + () => + fail( + new AntigravityError("Antigravity did not initialize within 30 seconds", "STARTUP_TIMEOUT"), + ), + 30_000, + ); + child.stderr.on("data", (chunk: Buffer) => { + stderr = (stderr + chunk.toString()).slice(-8192); + }); + child.stdin.on("error", (error: Error) => + fail(new AntigravityError(error.message, "STDIN_ERROR")), + ); + child.on("error", (error: Error) => fail(new AntigravityError(error.message, "SPAWN_ERROR"))); + child.on("close", (code, signal) => { + clearTimeout(startupDeadline); + lines.close(); + const error = new AntigravityError( + diagnostic(stderr.trim() || `Antigravity exited (${signal || code})`), + "PROCESS_EXIT", + ); + if (state === "starting") rejectReady(error); + if (state === "ready") options.onExit(error); + state = "exited"; + resolveExit(); + }); + lines.on("line", (line) => { + if (!line.trim() || state === "exited" || state === "stopping") return; + let decoded: Frame; + try { + decoded = decodeFrame(line); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + fail(new AntigravityError(`Invalid Antigravity output: ${message}`, "INVALID_FRAME")); + return; + } + if (state === "starting") { + if (decoded.event === "init") { + clearTimeout(startupDeadline); + state = "ready"; + resolveReady(decoded); + } else if (decoded.event === "result") { + fail(new AntigravityError(diagnostic(decoded.result.error), "STARTUP_ERROR")); + } + return; + } + options.onFrame(decoded); + }); + + function fail(failure: AntigravityError): void { + clearTimeout(startupDeadline); + if (state === "starting") rejectReady(failure); + if (state === "ready") options.onExit(failure); + if (state === "exited" || state === "stopping") return; + state = "stopping"; + cleanup = Promise.resolve(signalGroup(child, "SIGKILL")); + // The stop operation awaits cleanup and owns reporting its failure. + void cleanup.catch(() => undefined); + } + + return { + ready, + selection: selection(options.config), + prompt(text) { + if (state !== "ready") + return Promise.reject(new AntigravityError("Antigravity is not ready")); + return new Promise((resolve, reject) => { + child.stdin.write(encodePrompt(text), (error) => { + if (error) reject(error); + else resolve(); + }); + }); + }, + async stop(reason) { + if (process.platform === "win32") await cleanup; + if (state === "exited") return; + if (state === "starting") rejectReady(new AntigravityError("Antigravity startup canceled")); + clearTimeout(startupDeadline); + state = "stopping"; + if (process.platform === "win32") { + // Node signal emulation kills only the leader; taskkill must see the live tree. + cleanup = Promise.resolve(signalGroup(child, "SIGKILL")); + await cleanup; + await exited; + return; + } + if (reason === "close") child.stdin.end(); + else signalGroup(child, "SIGINT"); + const terminate = setTimeout(() => signalGroup(child, "SIGTERM"), 1000); + const kill = setTimeout(() => signalGroup(child, "SIGKILL"), 2000); + await exited; + clearTimeout(terminate); + clearTimeout(kill); + // A tool can keep running after its CLI leader exits. + signalGroup(child, "SIGKILL"); + }, + }; +} + +function signalGroup( + child: ChildProcessWithoutNullStreams, + signal: NodeJS.Signals, +): void | Promise { + if (!child.pid) return; + return signalProcess({ platform: process.platform, pid: child.pid, signal }); +} + +interface ProbeOptions { + launch: ProviderLaunch; + args: string[]; + cwd?: string; +} +export function probe(options: ProbeOptions): Promise { + return new Promise((resolve, reject) => { + const child = spawn(options.launch.command, [...options.launch.args, ...options.args], { + env: options.launch.env, + cwd: options.cwd, + stdio: "pipe", + detached: process.platform !== "win32", + }); + let stdout = ""; + let stderr = ""; + const deadline = setTimeout(() => { + const termination = Promise.resolve(signalGroup(child, "SIGKILL")); + void termination.then(() => { + reject(new AntigravityError("Antigravity discovery timed out", "PROBE_TIMEOUT")); + return undefined; + }, reject); + }, 10_000); + child.stdout.on("data", (chunk: Buffer) => { + stdout += chunk.toString(); + }); + child.stderr.on("data", (chunk: Buffer) => { + stderr = (stderr + chunk.toString()).slice(-8192); + }); + child.on("error", (error: Error) => { + clearTimeout(deadline); + reject(new AntigravityError(error.message, "SPAWN_ERROR")); + }); + child.on("close", (code) => { + clearTimeout(deadline); + if (code === 0) resolve(stdout); + else + reject( + new AntigravityError( + diagnostic(stderr.trim() || `agy exited with code ${code}`), + "PROBE_ERROR", + ), + ); + }); + }); +} diff --git a/plugins/antigravity-provider/server/internal/prompt.ts b/plugins/antigravity-provider/server/internal/prompt.ts new file mode 100644 index 0000000000..07f4298b35 --- /dev/null +++ b/plugins/antigravity-provider/server/internal/prompt.ts @@ -0,0 +1,64 @@ +import { randomUUID } from "node:crypto"; +import { chmod, mkdtemp, rm, writeFile } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import type { ProviderPrompt } from "@getpaseo/plugin/server/provider"; +import { AntigravityError } from "./wire.js"; + +interface EncodedPrompt { + text: string; + nativeText: string; +} + +export class PromptFiles { + private directory: string | null = null; + + async encode(prompt: ProviderPrompt): Promise { + if ( + prompt.input.type !== "message" || + prompt.delivery === "steer" || + prompt.outputSchema !== undefined + ) + throw new AntigravityError("Antigravity supports messages only"); + const parts = prompt.input.content.map((part) => { + if (part.type === "text") return part; + if (part.type === "image") return { ...part, extension: imageExtension(part.mimeType) }; + throw new AntigravityError("Antigravity supports text and image content only"); + }); + const text: string[] = []; + const native: string[] = []; + for (const part of parts) { + if (part.type === "text") { + text.push(part.text); + native.push(part.text); + } else if (part.type === "image") { + const extension = part.extension; + if (this.directory === null) { + this.directory = await mkdtemp(path.join(os.tmpdir(), "paseo-antigravity-images-")); + await chmod(this.directory, 0o700); + } + const file = path.join(this.directory, `${randomUUID()}.${extension}`); + await writeFile(file, Buffer.from(part.data, "base64"), { flag: "wx", mode: 0o600 }); + await chmod(file, 0o600); + native.push(`Attached image: ${file}`); + } + } + return { text: text.join("\n"), nativeText: native.join("\n") }; + } + + async close(): Promise { + if (this.directory !== null) { + await rm(this.directory, { recursive: true, force: true }); + this.directory = null; + } + } +} + +function imageExtension(mimeType: string): string { + const match = /^image\/([a-z0-9][a-z0-9.+-]*)$/i.exec(mimeType); + if (!match) throw new AntigravityError(`Expected an image MIME type, received ${mimeType}`); + const subtype = match[1].toLowerCase(); + if (subtype === "jpeg") return "jpg"; + if (subtype === "svg+xml") return "svg"; + return subtype; +} diff --git a/plugins/antigravity-provider/server/internal/session.ts b/plugins/antigravity-provider/server/internal/session.ts new file mode 100644 index 0000000000..caf2a62eeb --- /dev/null +++ b/plugins/antigravity-provider/server/internal/session.ts @@ -0,0 +1,397 @@ +import { randomUUID } from "node:crypto"; +import { z } from "zod"; +import type { + ProviderCatalog, + ProviderConfigChanges, + ProviderEvent, + ProviderPersistence, + ProviderPrompt, + ProviderSessionConfig, + ProviderLaunch, + ProviderTimelineItem, + ProviderUsage, +} from "@getpaseo/plugin/server/provider"; +import { startDriver, selection, type Driver } from "./process.js"; +import { validateSelection } from "./catalog.js"; +import { PromptFiles } from "./prompt.js"; +import { toolItem } from "./timeline.js"; +import { AntigravityError, diagnostic, type Frame, type Step } from "./wire.js"; + +const persistenceSchema = z + .object({ version: z.literal(1), data: z.object({ conversationId: z.string().min(1) }).strict() }) + .strict(); +interface Turn { + id: string; + text: Map; + usageSteps: Set; + tools: Map>; + usage: Required>; +} +type State = + | { type: "dormant" } + | { type: "starting"; driver: Driver } + | { type: "idle"; driver: Driver } + | { type: "running"; driver: Driver; turn: Turn } + | { type: "stopping"; driver: Driver } + | { type: "closed" }; +interface SessionOptions { + id: string; + config: ProviderSessionConfig; + launch: ProviderLaunch; + catalog(): ProviderCatalog; + persistence?: ProviderPersistence; + emit(event: ProviderEvent): void; +} + +export class Session { + private state: State = { type: "dormant" }; + private config: ProviderSessionConfig; + private conversationId: string | null; + private firstMessage: boolean; + private readonly promptFiles = new PromptFiles(); + private readonly reportedDenials = new Map(); + + constructor(private readonly options: SessionOptions) { + this.config = { ...options.config, mode: options.config.mode ?? "full-access" }; + this.conversationId = options.persistence + ? persistenceSchema.parse(options.persistence).data.conversationId + : null; + this.firstMessage = this.conversationId === null; + validateSelection(this.config, options.catalog()); + } + + async open(requestId: string, capabilities: readonly string[]): Promise { + await this.ensureDriver(); + this.emit({ + type: "session.opened", + requestId, + sessionId: this.options.id, + capabilities, + restoration: "core", + persistence: this.config.persist ? this.persistence() : undefined, + cwd: this.config.cwd, + title: this.config.title, + }); + this.publishFullAccessNotice(); + this.publishConfig(); + this.emit({ type: "session.commands", sessionId: this.options.id, commands: [] }); + this.emit({ type: "session.ready", requestId, sessionId: this.options.id }); + } + + async prompt(prompt: ProviderPrompt): Promise { + if (this.state.type === "running") + throw new AntigravityError( + "Antigravity already has an active turn; wait for it to finish", + "TURN_ACTIVE", + ); + const { text, nativeText } = await this.promptFiles.encode(prompt); + await this.ensureDriver(); + if (this.state.type !== "idle") throw new AntigravityError("Antigravity session is not ready"); + const driver = this.state.driver; + const turn: Turn = { + id: randomUUID(), + text: new Map(), + usageSteps: new Set(), + tools: new Map(), + usage: { inputTokens: 0, outputTokens: 0, cachedInputTokens: 0 }, + }; + this.state = { type: "running", driver, turn }; + this.emit({ + type: "session.prompt_result", + sessionId: this.options.id, + clientMessageId: prompt.clientMessageId, + result: { type: "turn", turnId: turn.id }, + }); + this.emit({ + type: "session.turn", + sessionId: this.options.id, + turnId: turn.id, + state: "started", + }); + this.item({ + type: "user_message", + id: prompt.clientMessageId, + clientMessageId: prompt.clientMessageId, + text, + }); + const driverText = + this.firstMessage && this.config.systemPrompt + ? `${this.config.systemPrompt}\n\n${nativeText}` + : nativeText; + try { + await driver.prompt(driverText); + this.firstMessage = false; + } catch (error) { + if (!(error instanceof Error)) throw error; + this.failTurn(new AntigravityError(error.message)); + await driver.stop("interrupt"); + } + } + + configure(changes: ProviderConfigChanges): void { + const config = { + ...this.config, + model: changes.model === null ? undefined : (changes.model ?? this.config.model), + mode: changes.mode === null ? "full-access" : (changes.mode ?? this.config.mode), + thinkingOption: + changes.thinkingOption === null + ? undefined + : (changes.thinkingOption ?? this.config.thinkingOption), + }; + if (changes.settings && Object.keys(changes.settings).length > 0) + throw new AntigravityError("Antigravity has no configurable settings"); + validateSelection(config, this.options.catalog()); + this.config = config; + this.publishConfig(); + } + + async interrupt(): Promise { + if (this.state.type !== "running") return; + const { driver, turn } = this.state; + this.state = { type: "stopping", driver }; + await driver.stop("interrupt"); + this.finishTools(turn, "canceled"); + this.emit({ + type: "session.turn", + sessionId: this.options.id, + turnId: turn.id, + state: "canceled", + }); + this.state = { type: "dormant" }; + } + + async close(): Promise { + const state = this.state; + this.state = { type: "closed" }; + try { + if (state.type !== "dormant" && state.type !== "closed") await state.driver.stop("close"); + } finally { + await this.promptFiles.close(); + } + if (state.type === "running") { + this.finishTools(state.turn, "canceled"); + this.emit({ + type: "session.turn", + sessionId: this.options.id, + turnId: state.turn.id, + state: "canceled", + }); + } + } + + private publishFullAccessNotice(): void { + this.emit({ + type: "session.notice", + sessionId: this.options.id, + notice: { + id: `${this.options.id}:full-access`, + severity: "warning", + title: "Antigravity is running with full access", + description: + "Antigravity's CLI cannot ask for permission when another app drives it, so Paseo starts it with --dangerously-skip-permissions. Every tool call, including shell commands, runs without asking.", + }, + }); + } + + private async ensureDriver(): Promise { + if (this.state.type === "closed") throw new AntigravityError("Antigravity session is closed"); + if (this.state.type === "stopping") { + await this.state.driver.stop("interrupt"); + this.state = { type: "dormant" }; + } + if (this.state.type === "idle" && this.state.driver.selection !== selection(this.config)) { + const driver = this.state.driver; + this.state = { type: "stopping", driver }; + await driver.stop("close"); + this.state = { type: "dormant" }; + } + if (this.state.type !== "dormant") return; + this.reportedDenials.clear(); + const driver = startDriver({ + launch: this.options.launch, + config: this.config, + conversationId: this.conversationId, + onFrame: (frame) => this.accept(frame), + onExit: (error) => this.failTurn(error), + }); + this.state = { type: "starting", driver }; + try { + const init = await driver.ready; + if (this.conversationId !== null && init.conversation_id !== this.conversationId) + throw new AntigravityError( + "Antigravity restored a different conversation", + "INVALID_RESUME", + ); + this.conversationId = init.conversation_id; + this.state = { type: "idle", driver }; + if (this.config.persist) + this.emit({ + type: "session.persistence", + sessionId: this.options.id, + persistence: this.persistence(), + }); + } catch (error) { + this.state = { type: "dormant" }; + await driver.stop("interrupt"); + throw error; + } + } + + private accept(frame: Frame): void { + if (this.state.type !== "running") return; + const { turn, driver } = this.state; + if (frame.event === "step_update") { + this.acceptStep(frame.step_update, turn); + return; + } + if (frame.event !== "result") return; + const result = frame.result; + const denied = takeNewDenials(result.denied_actions, this.reportedDenials); + if (denied.length > 0) { + this.emit({ + type: "session.notice", + sessionId: this.options.id, + notice: { + id: `${turn.id}:denied`, + severity: "warning", + title: denialNoticeTitle(denied), + description: "Antigravity's own policy denied it.", + }, + }); + } + if (result.status === "ERROR") { + const canceled = result.error === "interrupted"; + this.finishTools(turn, canceled ? "canceled" : "failed"); + this.retire(driver); + this.emit({ + type: "session.turn", + sessionId: this.options.id, + turnId: turn.id, + state: canceled ? "canceled" : "failed", + error: { message: diagnostic(result.error) }, + }); + } else { + this.finishTools(turn, "completed"); + this.state = { type: "idle", driver }; + this.emit({ + type: "session.turn", + sessionId: this.options.id, + turnId: turn.id, + state: "completed", + }); + } + } + + private acceptStep(step: Step, turn: Turn): void { + const id = `${turn.id}:${step.step_index}`; + if (step.step_type === "agent_response") { + const previous = turn.text.get(step.step_index) || ""; + const text = previous + step.text_delta; + turn.text.set(step.step_index, text); + if (step.text_delta) this.item({ type: "assistant_message", id, messageId: id, text }); + if (step.state === "DONE" && step.usage && !turn.usageSteps.has(step.step_index)) { + turn.usageSteps.add(step.step_index); + turn.usage.inputTokens += step.usage.input_tokens; + turn.usage.outputTokens += step.usage.output_tokens; + turn.usage.cachedInputTokens += step.usage.cache_read_tokens; + this.emit({ + type: "session.usage", + sessionId: this.options.id, + turnId: turn.id, + usage: { ...turn.usage }, + }); + } + } else if (step.step_type === "tool" || step.step_type === "subagent") { + const item = toolItem(step, id); + turn.tools.set(id, item); + this.item(item); + } + } + + private failTurn(error: AntigravityError): void { + const state = this.state; + if (state.type !== "running" && state.type !== "idle") return; + this.retire(state.driver); + if (state.type === "running") { + this.finishTools(state.turn, "failed"); + this.emit({ + type: "session.turn", + sessionId: this.options.id, + turnId: state.turn.id, + state: "failed", + error: { message: error.message, code: error.code }, + }); + } else + this.emit({ + type: "session.runtime_failed", + sessionId: this.options.id, + error: { message: error.message, code: error.code }, + }); + } + + private retire(driver: Driver): void { + this.state = { type: "stopping", driver }; + void driver.stop("interrupt").then(() => { + if (this.state.type === "stopping" && this.state.driver === driver) + this.state = { type: "dormant" }; + return undefined; + }); + } + + private finishTools(turn: Turn, status: "completed" | "failed" | "canceled"): void { + for (const item of turn.tools.values()) { + if (item.status !== "running") continue; + if (status === "failed") this.item({ ...item, status, error: "Antigravity turn failed" }); + else this.item({ ...item, status, error: null }); + } + } + + private persistence(): ProviderPersistence { + if (this.conversationId === null) throw new AntigravityError("Antigravity has no conversation"); + return { version: 1, data: { conversationId: this.conversationId } }; + } + + private publishConfig(): void { + this.emit({ + type: "session.config", + sessionId: this.options.id, + config: { + model: this.config.model, + mode: this.config.mode || "full-access", + models: this.options.catalog().models, + modes: this.options.catalog().modes, + thinkingOptions: [], + settings: [], + }, + }); + } + private item(item: ProviderTimelineItem): void { + this.emit({ type: "timeline.item", sessionId: this.options.id, item }); + } + private emit(event: ProviderEvent): void { + this.options.emit(event); + } +} + +// Native results contain every denial since this driver started, including repeated actions. +function takeNewDenials( + actions: Extract["result"]["denied_actions"], + reported: Map, +) { + const counts = new Map(); + const fresh = actions.filter((action) => { + const key = JSON.stringify([action.action, action.display_name]); + const count = (counts.get(key) ?? 0) + 1; + counts.set(key, count); + return count > (reported.get(key) ?? 0); + }); + for (const [key, count] of counts) reported.set(key, Math.max(count, reported.get(key) ?? 0)); + return fresh; +} + +function denialNoticeTitle( + actions: Extract["result"]["denied_actions"], +): string { + if (actions.length > 1) return `${actions.length} actions denied`; + return actions[0].action === "command" ? "Shell command denied" : "Action denied"; +} diff --git a/plugins/antigravity-provider/server/internal/signals.ts b/plugins/antigravity-provider/server/internal/signals.ts new file mode 100644 index 0000000000..be34ecc8c9 --- /dev/null +++ b/plugins/antigravity-provider/server/internal/signals.ts @@ -0,0 +1,40 @@ +import { execFile } from "node:child_process"; +import { promisify } from "node:util"; + +interface SignalOptions { + platform: NodeJS.Platform; + pid: number; + signal: NodeJS.Signals; +} + +type SignalPlan = + | { type: "group"; pid: number; signal: NodeJS.Signals } + | { type: "tree"; command: "taskkill"; args: string[] }; + +const executeFile = promisify(execFile); + +export function signalPlan(options: SignalOptions): SignalPlan { + if (options.platform === "win32") { + return { type: "tree", command: "taskkill", args: ["/PID", String(options.pid), "/T", "/F"] }; + } + return { type: "group", pid: -options.pid, signal: options.signal }; +} + +export function signalProcess(options: SignalOptions): void | Promise { + const plan = signalPlan(options); + if (plan.type === "tree") return killTree(plan); + try { + process.kill(plan.pid, plan.signal); + } catch (error) { + if (!(error instanceof Error && "code" in error && error.code === "ESRCH")) throw error; + } +} + +async function killTree(plan: Extract): Promise { + try { + await executeFile(plan.command, plan.args, { windowsHide: true }); + } catch (error) { + // taskkill reports an already-exited PID with exit code 128. + if (!(error instanceof Error && "code" in error && error.code === 128)) throw error; + } +} diff --git a/plugins/antigravity-provider/server/internal/timeline.ts b/plugins/antigravity-provider/server/internal/timeline.ts new file mode 100644 index 0000000000..22cc81c643 --- /dev/null +++ b/plugins/antigravity-provider/server/internal/timeline.ts @@ -0,0 +1,55 @@ +import type { + ProviderTimelineItem, + ProviderToolCallDetail, +} from "@getpaseo/plugin/server/provider"; +import type { Step } from "./wire.js"; + +export function toolItem( + step: Step, + id: string, +): Extract { + const detail = toolDetail(step); + const name = step.tool_name || "Antigravity tool"; + const metadata = step.subagent_info ? { subagents: step.subagent_info.subagents } : undefined; + const base = { type: "tool_call" as const, id, callId: id, name, detail, metadata }; + if (step.state === "ERROR") { + const error = + step.tool_info && step.tool_info.error + ? step.tool_info.error.message + : "Antigravity tool failed"; + return { ...base, status: "failed", error }; + } + const status = step.state === "ACTIVE" ? "running" : "completed"; + return { ...base, status, error: null }; +} + +function toolDetail(step: Step): ProviderToolCallDetail { + if (step.subagent_info) { + const child = step.subagent_info.subagents[0]; + if (child) + return { + type: "sub_agent", + subAgentType: child.type_name, + description: child.initial_prompt, + childSessionId: child.conversation_id === null ? undefined : child.conversation_id, + log: child.log_uri, + }; + } + const info = step.tool_info; + if (!info) return { type: "plain_text", label: step.tool_name, text: "" }; + const path = info.parameters.AbsolutePath || info.parameters.TargetFile; + if (info.name === "run_command" && typeof info.parameters.CommandLine === "string") { + return { type: "shell", command: info.parameters.CommandLine, output: info.output }; + } + if (info.name === "view_file" && typeof path === "string") + return { type: "read", filePath: path, content: info.output }; + if (info.name === "write_to_file" && typeof path === "string") + return { type: "write", filePath: path }; + if ( + (info.name === "replace_file_content" || info.name === "multi_replace_file_content") && + typeof path === "string" + ) + return { type: "edit", filePath: path }; + // Native frames summarize writes; they do not carry the file contents or diff. + return { type: "unknown", input: info.parameters, output: info.error || info.output }; +} diff --git a/plugins/antigravity-provider/server/internal/wire.ts b/plugins/antigravity-provider/server/internal/wire.ts new file mode 100644 index 0000000000..eef417c83c --- /dev/null +++ b/plugins/antigravity-provider/server/internal/wire.ts @@ -0,0 +1,85 @@ +import { z } from "zod"; + +const usage = z.object({ + input_tokens: z.number().nonnegative(), + output_tokens: z.number().nonnegative(), + cache_read_tokens: z.number().nonnegative().default(0), +}); +const tool = z.object({ + name: z.string(), + parameters: z.record(z.string(), z.json()).default({}), + output: z.string().default(""), + error: z.object({ type: z.string(), message: z.string() }).nullable().default(null), +}); +const child = z.object({ + type_name: z.string(), + role: z.string().default(""), + initial_prompt: z.string().default(""), + conversation_id: z.string().nullable().default(null), + log_uri: z.string().default(""), + workspace_uris: z.array(z.string()).default([]), +}); +const frame = z.discriminatedUnion("event", [ + z.object({ + event: z.literal("init"), + conversation_id: z.string().min(1), + init: z.object({ model: z.string().nullable().default(null) }), + }), + z.object({ + event: z.literal("step_update"), + step_update: z.object({ + conversation_id: z.string(), + step_index: z.number().int().nonnegative(), + state: z.enum(["ACTIVE", "DONE", "ERROR"]), + step_type: z.string(), + text_delta: z.string().default(""), + tool_name: z.string().default(""), + tool_info: tool.nullable().default(null), + subagent_info: z + .object({ subagents: z.array(child) }) + .nullable() + .default(null), + usage: usage.nullable().default(null), + }), + }), + z.object({ + event: z.literal("result"), + result: z.object({ + conversation_id: z.string(), + status: z.enum(["SUCCESS", "ERROR"]), + response: z.string().default(""), + error: z.string().default("Antigravity turn failed"), + denied_actions: z + .array(z.object({ action: z.string(), display_name: z.string() })) + .default([]), + }), + }), +]); +export type Frame = z.infer; +export type Step = Extract["step_update"]; +export type Init = Extract; + +export function decodeFrame(line: string): Frame { + return frame.parse(JSON.parse(line)); +} + +export function encodePrompt(text: string): string { + return `${JSON.stringify({ event: "user", message: { content: text } })}\n`; +} + +export class AntigravityError extends Error { + constructor( + message: string, + readonly code = "ANTIGRAVITY_ERROR", + ) { + super(message); + this.name = "AntigravityError"; + } +} + +export function diagnostic(message: string): string { + if (/auth|sign.in|log.in/i.test(message)) { + return "Antigravity authentication failed. Run `agy` and sign in, then retry."; + } + return message; +} diff --git a/plugins/antigravity-provider/server/provider.test.ts b/plugins/antigravity-provider/server/provider.test.ts new file mode 100644 index 0000000000..8ba12841e0 --- /dev/null +++ b/plugins/antigravity-provider/server/provider.test.ts @@ -0,0 +1,775 @@ +import { mkdtemp, readFile, rm, stat } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { afterEach, describe, expect, it } from "vitest"; +import { + PROVIDER_CAPABILITIES, + ProviderEventSchema, + type ProviderConnection, + type ProviderContent, + type ProviderEvent, + type ProviderInput, + type ProviderLaunch, + type ProviderPersistence, +} from "@getpaseo/plugin/server/provider"; +import { signalPlan } from "./internal/signals.js"; +import { createAntigravityProvider } from "./provider.js"; + +const temporary: string[] = []; +const connections: ProviderConnection[] = []; +const fake = fileURLToPath(new URL("../test/agy.mjs", import.meta.url)); +const provider = createAntigravityProvider(); + +it("publishes a monochrome, theme-aware provider icon", async () => { + const icon = await readFile(new URL("../icon.svg", import.meta.url), "utf8"); + expect(icon).toContain('viewBox="0 0 24 24"'); + expect(icon).toContain('fill="currentColor"'); + expect(icon).not.toMatch(/#[\da-f]{3,8}\b|rgba?\(|hsla?\(|url\(/i); + expect(icon).not.toMatch(/<(?:style|mask|defs|linearGradient|radialGradient)\b|\bid=/i); + expect([...icon.matchAll(/\b(?:fill|stroke)="([^"]+)"/g)].map((match) => match[1])).toEqual([ + "currentColor", + ]); +}); + +afterEach(async () => { + await Promise.all(connections.splice(0).map((connection) => connection.close())); + await Promise.all( + temporary.splice(0).map((directory) => rm(directory, { recursive: true, force: true })), + ); +}); + +async function harness(env: Record = {}) { + const cwd = await mkdtemp(path.join(os.tmpdir(), "antigravity-test-")); + temporary.push(cwd); + const log = path.join(cwd, "argv.ndjson"); + const launch: ProviderLaunch = { + command: process.execPath, + args: [fake, "--launch-prefix"], + env: { AGY_TEST_LOG: log, ...env }, + }; + const connection = await provider.connect({ + versions: [1], + capabilities: PROVIDER_CAPABILITIES, + launch, + }); + connections.push(connection); + const events: ProviderEvent[] = []; + connection.onEvent((event) => { + ProviderEventSchema.parse(event); + events.push(event); + }); + async function wait(predicate: (event: ProviderEvent) => boolean) { + await expect.poll(() => events.some(predicate), { timeout: 5000, interval: 10 }).toBe(true); + const event = events.find(predicate); + if (!event) throw new Error("Expected event missing"); + return event; + } + async function request(input: ProviderInput) { + await connection.send(input); + if ("requestId" in input) + await wait( + (event) => + "requestId" in event && + event.requestId === input.requestId && + (event.type === "request.completed" || + event.type === "request.failed" || + event.type === "session.ready" || + event.type === "catalog"), + ); + } + async function open(restored?: ProviderPersistence, sessionId = "s") { + await request({ + type: "session.open", + requestId: `open-${events.length}`, + sessionId, + persistence: restored, + history: "replay", + config: { + cwd, + env: { AGY_SESSION_VALUE: "session-overlay" }, + settings: {}, + mcpServers: {}, + persist: true, + systemPrompt: "SYSTEM PREFIX", + }, + }); + } + async function message(content: ProviderContent[], sessionId = "s") { + const clientMessageId = `prompt-${events.length}`; + await connection.send({ + type: "session.prompt", + sessionId, + prompt: { + clientMessageId, + delivery: "auto", + input: { type: "message", content }, + }, + }); + await wait( + (event) => + event.type === "session.prompt_result" && event.clientMessageId === clientMessageId, + ); + return clientMessageId; + } + function prompt(text: string) { + return message([{ type: "text", text }]); + } + async function completed(clientMessageId: string) { + const result = events.find( + (event) => + event.type === "session.prompt_result" && event.clientMessageId === clientMessageId, + ); + if (!result || result.type !== "session.prompt_result" || result.result.type !== "turn") + throw new Error("Prompt was not admitted"); + const turnId = result.result.turnId; + return wait( + (event) => + event.type === "session.turn" && event.turnId === turnId && event.state !== "started", + ); + } + async function records() { + return (await readFile(log, "utf8")) + .trim() + .split("\n") + .map((line) => JSON.parse(line)); + } + function persistence() { + const opened = events.find((event) => event.type === "session.opened"); + if (!opened || opened.type !== "session.opened" || !opened.persistence) + throw new Error("Missing persistence"); + return opened.persistence; + } + return { + connection, + events, + launch, + cwd, + wait, + request, + open, + prompt, + message, + completed, + records, + persistence, + }; +} + +it("streams complete text snapshots, prefixes the system prompt only once and preserves launch argv/env", async () => { + const h = await harness(); + expect(h.connection.capabilities).toEqual([ + "prompt.message", + "prompt.image", + "session.configure", + "session.persistence", + ]); + await h.open(); + await h.completed(await h.prompt("HELLO")); + await h.completed(await h.prompt("HELLO AGAIN")); + const text = h.events.filter( + (event) => event.type === "timeline.item" && event.item.type === "assistant_message", + ); + expect( + text.map((event) => + event.type === "timeline.item" && event.item.type === "assistant_message" + ? event.item.text + : "", + ), + ).toEqual(["PHASE0_HELLO", "PHASE0_HELLO\n", "PHASE0_HELLO", "PHASE0_HELLO\n"]); + const records = await h.records(); + expect(records.find((entry) => entry.args.includes("--input-format")).args).toEqual([ + "--launch-prefix", + "--input-format", + "stream-json", + "--output-format", + "stream-json", + "--add-dir", + h.cwd, + "--disable-slash-commands", + "--dangerously-skip-permissions", + ]); + expect(records.find((entry) => entry.args.includes("--input-format")).env).toBe( + "session-overlay", + ); + expect( + records.filter((entry) => entry.input).map((entry) => entry.input.message.content), + ).toEqual(["SYSTEM PREFIX\n\nHELLO", "HELLO AGAIN"]); +}); + +it("maps tools and emits one denial notice for a SUCCESS result", async () => { + const h = await harness(); + await h.open(); + // This capture contains three distinct native results, one for each submitted prompt. + await h.completed(await h.prompt("TOOLS")); + await h.completed(await h.prompt("TOOLS")); + const result = await h.completed(await h.prompt("TOOLS")); + expect(result).toMatchObject({ type: "session.turn", state: "completed" }); + const tools = h.events.filter( + (event) => event.type === "timeline.item" && event.item.type === "tool_call", + ); + expect(tools).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + item: expect.objectContaining({ + name: "view_file", + status: "completed", + detail: expect.objectContaining({ type: "read" }), + }), + }), + expect.objectContaining({ + item: expect.objectContaining({ + name: "write_to_file", + status: "completed", + detail: expect.objectContaining({ type: "write" }), + }), + }), + expect.objectContaining({ + item: expect.objectContaining({ + name: "run_command", + status: "failed", + detail: expect.objectContaining({ type: "shell", command: "printf PHASE0_SHELL" }), + }), + }), + ]), + ); + const notices = h.events.filter( + (event) => event.type === "session.notice" && event.notice.id !== "s:full-access", + ); + expect(notices).toHaveLength(1); + expect(notices[0]).toMatchObject({ + notice: { + title: "Shell command denied", + description: "Antigravity's own policy denied it.", + }, + }); +}); + +it("reports only new process denials across replayed turns and resets after respawn", async () => { + const h = await harness(); + await h.open(); + await h.completed(await h.prompt("DENIAL_REPLAY")); + expect( + h.events.filter( + (event) => event.type === "session.notice" && event.notice.id !== "s:full-access", + ), + ).toHaveLength(1); + const followupStart = h.events.length; + await h.completed(await h.prompt("DENIAL_REPLAY")); + const followup = h.events.slice(followupStart); + expect( + followup.some( + (event) => event.type === "session.notice" && event.notice.id !== "s:full-access", + ), + ).toBe(false); + expect(followup).toContainEqual( + expect.objectContaining({ + type: "timeline.item", + item: expect.objectContaining({ type: "assistant_message", text: "FOLLOWUP_OK\n" }), + }), + ); + await h.request({ + type: "session.configure", + sessionId: "s", + requestId: "mode", + changes: { model: "gemini-3.8-flash-low" }, + }); + await h.completed(await h.prompt("DENIAL_REPLAY")); + expect( + h.events.filter( + (event) => event.type === "session.notice" && event.notice.id !== "s:full-access", + ), + ).toHaveLength(2); + expect( + (await h.records()).filter((entry) => entry.args?.includes("--input-format")), + ).toHaveLength(2); +}); + +it("counts repeated native action denials without exposing tool identifiers", async () => { + const h = await harness({ AGY_TEST_DENIAL_COUNTS: "2,2,3" }); + await h.open(); + await h.completed(await h.prompt("DENIAL_REPLAY")); + await h.completed(await h.prompt("DENIAL_REPLAY")); + const notices = h.events.filter( + (event) => event.type === "session.notice" && event.notice.id !== "s:full-access", + ); + expect(notices).toHaveLength(1); + expect(notices[0]).toMatchObject({ + notice: { + title: "2 actions denied", + description: "Antigravity's own policy denied it.", + }, + }); + await h.completed(await h.prompt("DENIAL_REPLAY")); + expect( + h.events.filter( + (event) => event.type === "session.notice" && event.notice.id !== "s:full-access", + ), + ).toHaveLength(2); + expect( + h.events.findLast( + (event) => event.type === "session.notice" && event.notice.id !== "s:full-access", + ), + ).toMatchObject({ + notice: { title: "Shell command denied" }, + }); +}); + +it("accounts DONE generation usage once without adding cumulative results, including resume", async () => { + const h = await harness(); + await h.open(); + await h.completed(await h.prompt("MULTI")); + await h.completed(await h.prompt("MULTI")); + const usages = h.events.filter((event) => event.type === "session.usage"); + expect( + usages.map((event) => (event.type === "session.usage" ? event.usage.inputTokens : null)), + ).toEqual([11263, 11604]); + const persistence = h.persistence(); + await h.request({ type: "session.close", sessionId: "s", requestId: "close" }); + await h.open(persistence); + await h.completed(await h.prompt("RESUME")); + expect(h.events.filter((event) => event.type === "session.usage")).toHaveLength(3); + expect(h.events.findLast((event) => event.type === "session.usage")).toMatchObject({ + usage: { inputTokens: 11815 }, + }); +}); + +it("maps subagent steps to a tool carrying the child conversation ID", async () => { + const h = await harness(); + await h.open(); + await h.completed(await h.prompt("SUBAGENT")); + expect(h.events).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + type: "timeline.item", + item: expect.objectContaining({ + name: "invoke_subagent", + status: "completed", + detail: expect.objectContaining({ + type: "sub_agent", + childSessionId: "b6f02329-7415-49a7-a9a9-1cd09e898d26", + }), + }), + }), + ]), + ); +}); + +it.each(["HANG", "TOOL_HANG"])( + "interrupts %s, settles canceled, and respawns the same conversation", + async (text) => { + const h = await harness(); + await h.open(); + const message = await h.prompt(text); + await h.wait( + (event) => + event.type === "timeline.item" && + (event.item.type === "assistant_message" || event.item.type === "tool_call"), + ); + await h.request({ type: "session.interrupt", requestId: "interrupt", sessionId: "s" }); + expect(await h.completed(message)).toMatchObject({ state: "canceled" }); + await h.completed(await h.prompt("HELLO")); + const records = await h.records(); + const launches = records.filter((entry) => entry.args && entry.args.includes("--input-format")); + expect(launches).toHaveLength(2); + expect(launches[1].args.slice(-2)).toEqual([ + "--conversation", + "e5cf1e2d-c715-4328-8e67-4a85c8dc3cda", + ]); + // Windows taskkill terminates the tree without invoking the CLI's signal handlers. + expect(records.filter((entry) => entry.signal)).toEqual( + process.platform === "win32" ? [] : [{ signal: "SIGINT" }], + ); + const canceled = h.events.filter( + (event) => event.type === "session.turn" && event.state === "canceled", + ); + expect(canceled).toHaveLength(1); + }, +); + +it("stops an unresponsive driver and still respawns the conversation", async () => { + const h = await harness({ AGY_TEST_IGNORE_SIGNALS: "yes" }); + await h.open(); + const message = await h.prompt("HANG"); + await h.wait( + (event) => event.type === "timeline.item" && event.item.type === "assistant_message", + ); + await h.request({ type: "session.interrupt", requestId: "interrupt-timeout", sessionId: "s" }); + expect(await h.completed(message)).toMatchObject({ state: "canceled" }); + expect((await h.records()).filter((entry) => entry.signal)).toEqual( + process.platform === "win32" ? [] : [{ signal: "SIGINT" }, { signal: "SIGTERM" }], + ); + await h.completed(await h.prompt("HELLO")); +}); + +it("rejects plan and keeps the full-access selection without respawning", async () => { + const h = await harness(); + await h.open(); + await h.request({ + type: "session.configure", + requestId: "plan", + sessionId: "s", + changes: { mode: "plan" }, + }); + expect(h.events).toContainEqual( + expect.objectContaining({ + type: "request.failed", + requestId: "plan", + error: { code: "INVALID_MODE", message: "Unknown Antigravity mode: plan" }, + }), + ); + await h.request({ + type: "session.configure", + requestId: "full", + sessionId: "s", + changes: { mode: "full-access" }, + }); + await h.completed(await h.prompt("HELLO")); + expect( + (await h.records()).filter((entry) => entry.args?.includes("--input-format")), + ).toHaveLength(1); +}); + +it("emits one full-access warning per open, none on respawn, and one again on resume", async () => { + const h = await harness(); + await h.open(); + const warning = { + type: "session.notice", + sessionId: "s", + notice: { + id: "s:full-access", + severity: "warning", + title: "Antigravity is running with full access", + description: + "Antigravity's CLI cannot ask for permission when another app drives it, so Paseo starts it with --dangerously-skip-permissions. Every tool call, including shell commands, runs without asking.", + }, + }; + const opened = h.events.findIndex((event) => event.type === "session.opened"); + expect(h.events[opened + 1]).toEqual(warning); + expect(h.events.slice(0, opened).some((event) => event.type === "session.notice")).toBe(false); + await h.completed(await h.prompt("HELLO")); + await h.request({ + type: "session.configure", + requestId: "model", + sessionId: "s", + changes: { model: "gemini-3.8-flash-low" }, + }); + await h.completed(await h.prompt("HELLO")); + expect(h.events.filter((event) => event.type === "session.notice")).toEqual([warning]); + const launches = (await h.records()).filter((entry) => entry.args?.includes("--input-format")); + expect(launches).toHaveLength(2); + for (const launch of launches) { + expect(launch.args).toContain("--dangerously-skip-permissions"); + expect(launch.args).not.toContain("--mode"); + } + const persistence = h.persistence(); + await h.connection.close(); + const resumed = await harness(); + await resumed.open(persistence); + await resumed.completed(await resumed.prompt("RESUME")); + expect(resumed.events.filter((event) => event.type === "session.notice")).toEqual([warning]); +}); + +it("defers model and mode changes until the next prompt, then respawns with validated flags", async () => { + const h = await harness(); + await h.open(); + await h.request({ + type: "session.configure", + requestId: "change", + sessionId: "s", + changes: { model: "gemini-3.8-flash-low", mode: "full-access" }, + }); + expect( + (await h.records()).filter((entry) => entry.args && entry.args.includes("--input-format")), + ).toHaveLength(1); + await h.completed(await h.prompt("HELLO")); + const launches = (await h.records()).filter( + (entry) => entry.args && entry.args.includes("--input-format"), + ); + expect(launches).toHaveLength(2); + expect(launches[1].args.slice(-4)).toEqual([ + "--model", + "gemini-3.8-flash-low", + "--conversation", + "e5cf1e2d-c715-4328-8e67-4a85c8dc3cda", + ]); + await h.request({ + type: "session.configure", + requestId: "invalid", + sessionId: "s", + changes: { model: "unknown" }, + }); + expect(h.events).toContainEqual( + expect.objectContaining({ + type: "request.failed", + requestId: "invalid", + error: expect.objectContaining({ code: "INVALID_MODEL" }), + }), + ); +}); + +it("resumes after connection close without replay or another system prefix", async () => { + const first = await harness(); + await first.open(); + await first.completed(await first.prompt("HELLO")); + const persistence = first.persistence(); + await first.connection.close(); + const second = await harness(); + await second.open(persistence); + expect(second.events.filter((event) => event.type === "timeline.item")).toEqual([]); + await second.completed(await second.prompt("RESUME")); + expect( + (await second.records()) + .filter((entry) => entry.input) + .map((entry) => entry.input.message.content), + ).toEqual(["RESUME"]); +}); + +it.each(["auth", "exit", "stderr"])( + "reports startup %s without waiting for init forever", + async (startup) => { + const h = await harness({ AGY_TEST_STARTUP: startup }); + await h.open(); + expect(h.events.filter((event) => event.type === "session.ready")).toEqual([]); + const failure = h.events.find((event) => event.type === "request.failed"); + expect(failure).toMatchObject({ + type: "request.failed", + error: { + message: expect.stringMatching(startup === "exit" ? /exited/ : /Run `agy` and sign in/), + }, + }); + }, +); + +it("rejects mid-turn steering and reports a busy message without queueing a second native turn", async () => { + const h = await harness(); + await h.open(); + await h.prompt("HANG"); + const second = await h.prompt("SECOND"); + expect( + h.events.find( + (event) => event.type === "session.prompt_result" && event.clientMessageId === second, + ), + ).toMatchObject({ + type: "session.prompt_result", + result: { type: "failed", error: { code: "TURN_ACTIVE" } }, + }); + await expect( + h.connection.send({ + type: "session.prompt", + sessionId: "s", + prompt: { + clientMessageId: "steer", + delivery: "steer", + input: { type: "message", content: [{ type: "text", text: "x" }] }, + }, + }), + ).rejects.toThrow(/prompt.steer/); +}); + +describe("discovery", () => { + it("reuses one discovered catalog for two opens and configure, and refreshes explicit catalog requests", async () => { + const h = await harness(); + await h.open(); + await h.open(undefined, "second"); + await h.request({ + type: "session.configure", + requestId: "configure-cached", + sessionId: "second", + changes: { model: "gemini-3.8-flash-low" }, + }); + expect( + (await h.records()).filter((entry) => entry.args && entry.args.includes("models")), + ).toHaveLength(1); + await h.request({ type: "catalog", requestId: "refresh" }); + expect( + (await h.records()).filter((entry) => entry.args && entry.args.includes("models")), + ).toHaveLength(2); + }); + it.each(["1.1.15", "1.2.12", "1.2.13"])("accepts driver-capable version %s", async (version) => { + const h = await harness({ AGY_TEST_VERSION: version }); + expect(await provider.status?.({ launch: h.launch })).toEqual({ available: true }); + }); + + it("parses tab-separated catalog and exposes only full access without thinking options", async () => { + const h = await harness(); + await h.request({ type: "catalog", requestId: "catalog" }); + const event = h.events[0]; + expect(event).toMatchObject({ + type: "catalog", + catalog: { + models: expect.arrayContaining([ + { id: "gemini-3.8-flash-low", label: "Gemini 3.8 Flash (Low)" }, + ]), + thinkingOptions: [], + defaultMode: "full-access", + }, + }); + if (event.type !== "catalog") throw new Error("Missing catalog"); + expect(event.catalog.models).toHaveLength(11); + expect(event.catalog.modes).toEqual([ + { + id: "full-access", + label: "Full access", + icon: "ShieldOff", + colorTier: "dangerous", + isUnattended: true, + description: + "Antigravity cannot ask for permission when another app drives it. Paseo starts it with --dangerously-skip-permissions.", + }, + ]); + }); + it("reports missing, old, unauthenticated and available launches", async () => { + expect(await provider.status?.({})).toMatchObject({ + available: false, + diagnostic: expect.stringContaining("agy not found"), + }); + const missing = await harness(); + expect( + await provider.status?.({ + launch: { ...missing.launch, command: path.join(missing.cwd, "missing-agy") }, + }), + ).toMatchObject({ available: false, diagnostic: expect.stringContaining("ENOENT") }); + const old = await harness({ AGY_TEST_VERSION: "1.1.14" }); + expect(await provider.status?.({ launch: old.launch })).toMatchObject({ + available: false, + diagnostic: expect.stringContaining("1.1.15"), + }); + const unauthenticated = await harness({ AGY_TEST_AUTH: "missing" }); + expect(await provider.status?.({ launch: unauthenticated.launch })).toEqual({ + available: false, + diagnostic: "Antigravity authentication failed. Run `agy` and sign in, then retry.", + }); + const authenticated = await harness(); + expect(await provider.status?.({ launch: authenticated.launch })).toEqual({ available: true }); + }); + it("keys catalog caching by the complete effective launch without exposing env values", async () => { + const h = await harness(); + const key = await provider.getCatalogCacheKey?.({ scope: "global", launch: h.launch }); + expect(key).toMatch(/^[a-f0-9]{64}$/); + expect( + await provider.getCatalogCacheKey?.({ + scope: "workspace", + cwd: h.cwd, + force: true, + launch: h.launch, + }), + ).toBe(key); + expect( + await provider.getCatalogCacheKey?.({ + scope: "global", + launch: { ...h.launch, args: [...h.launch.args, "changed"] }, + }), + ).not.toBe(key); + expect( + await provider.getCatalogCacheKey?.({ + scope: "global", + launch: { ...h.launch, env: { ...h.launch.env, HOME: "different" } }, + }), + ).not.toBe(key); + }); +}); + +describe("process signal plans", () => { + it.each(["SIGINT", "SIGTERM", "SIGKILL"] as const)("kills the Windows tree for %s", (signal) => { + expect(signalPlan({ platform: "win32", pid: 123, signal })).toEqual({ + type: "tree", + command: "taskkill", + args: ["/PID", "123", "/T", "/F"], + }); + }); + it.each(["SIGINT", "SIGTERM", "SIGKILL"] as const)( + "preserves POSIX group signal %s", + (signal) => { + expect(signalPlan({ platform: "linux", pid: 123, signal })).toEqual({ + type: "group", + pid: -123, + signal, + }); + }, + ); +}); + +describe("image prompts", () => { + it.each(["session", "connection"])( + "encodes mixed content as private session files and cleans up on %s close", + async (close) => { + const h = await harness(); + await h.open(); + const png = await readFile( + new URL("../test/fixtures/image-file-external.png", import.meta.url), + ); + const jpeg = Buffer.from("image file bytes"); + await h.completed( + await h.message([ + { type: "text", text: "Describe these images." }, + { type: "image", data: png.toString("base64"), mimeType: "image/png" }, + { type: "text", text: "Compare them." }, + { type: "image", data: jpeg.toString("base64"), mimeType: "image/jpeg" }, + ]), + ); + const sent = (await h.records()).find((entry) => entry.input).input.message.content; + const lines = sent.split("\n"); + const pngPath = lines[3].slice("Attached image: ".length); + const jpegPath = lines[5].slice("Attached image: ".length); + expect(sent).toBe( + `SYSTEM PREFIX\n\nDescribe these images.\nAttached image: ${pngPath}\nCompare them.\nAttached image: ${jpegPath}`, + ); + expect(path.isAbsolute(pngPath)).toBe(true); + expect(path.extname(pngPath)).toBe(".png"); + expect(path.extname(jpegPath)).toBe(".jpg"); + expect(path.dirname(pngPath)).toBe(path.dirname(jpegPath)); + expect(path.dirname(pngPath)).not.toBe(h.cwd); + expect(await readFile(pngPath)).toEqual(png); + expect(await readFile(jpegPath)).toEqual(jpeg); + if (process.platform !== "win32") { + expect((await stat(path.dirname(pngPath))).mode & 0o777).toBe(0o700); + expect((await stat(pngPath)).mode & 0o777).toBe(0o600); + expect((await stat(jpegPath)).mode & 0o777).toBe(0o600); + } + const args = (await h.records()).find((entry) => entry.args?.includes("--input-format")).args; + expect(args.filter((arg: string) => arg === "--add-dir")).toHaveLength(1); + if (close === "connection") await h.connection.close(); + else await h.request({ type: "session.close", requestId: "close-images", sessionId: "s" }); + await expect(stat(path.dirname(pngPath))).rejects.toMatchObject({ code: "ENOENT" }); + }, + ); + + it("keeps image-only prompts in distinct session directories until each session closes", async () => { + const h = await harness(); + await h.open(); + await h.open(undefined, "other"); + const content: ProviderContent[] = [ + { type: "image", data: "aW1hZ2U=", mimeType: "image/webp" }, + ]; + await h.completed(await h.message(content)); + await h.completed(await h.message(content)); + await h.completed(await h.message(content, "other")); + const paths = (await h.records()) + .filter((entry) => entry.input) + .map((entry) => entry.input.message.content.split("Attached image: ")[1]); + expect(new Set(paths).size).toBe(3); + expect(path.dirname(paths[0])).toBe(path.dirname(paths[1])); + expect(path.dirname(paths[0])).not.toBe(path.dirname(paths[2])); + expect(paths.every((file: string) => path.extname(file) === ".webp")).toBe(true); + await h.request({ type: "session.close", requestId: "close-first", sessionId: "s" }); + await expect(stat(path.dirname(paths[0]))).rejects.toMatchObject({ code: "ENOENT" }); + expect(await readFile(paths[2], "utf8")).toBe("image"); + await h.connection.close(); + await expect(stat(path.dirname(paths[2]))).rejects.toMatchObject({ code: "ENOENT" }); + }); + + it("rejects a non-image MIME type before writing to native stdin", async () => { + const h = await harness(); + await h.open(); + const id = await h.message([{ type: "image", data: "dGV4dA==", mimeType: "text/plain" }]); + expect( + h.events.find( + (event) => event.type === "session.prompt_result" && event.clientMessageId === id, + ), + ).toMatchObject({ + result: { type: "failed", error: { message: expect.stringContaining("image MIME type") } }, + }); + expect((await h.records()).filter((entry) => entry.input)).toHaveLength(0); + }); +}); diff --git a/plugins/antigravity-provider/server/provider.ts b/plugins/antigravity-provider/server/provider.ts new file mode 100644 index 0000000000..88c73c7faa --- /dev/null +++ b/plugins/antigravity-provider/server/provider.ts @@ -0,0 +1,171 @@ +import { createHash } from "node:crypto"; +import { + negotiateProviderCapabilities, + requireProviderCapabilities, + type ProviderCatalog, + type ProviderConnection, + type ProviderEvent, + type ProviderInput, + type ProviderLaunch, + type ProviderRegistration, +} from "@getpaseo/plugin/server/provider"; +import { getCatalog, getStatus } from "./internal/catalog.js"; +import { Session } from "./internal/session.js"; +import { AntigravityError } from "./internal/wire.js"; + +const capabilities = [ + "prompt.message", + "prompt.image", + "session.configure", + "session.persistence", +] as const; + +export function createAntigravityProvider(): ProviderRegistration { + return { + id: "antigravity", + label: "Antigravity", + icon: "icon.svg", + command: ["agy"], + description: "Antigravity CLI on your host", + async status({ launch }) { + if (!launch) + return { + available: false, + diagnostic: "agy not found on PATH. Install Antigravity, then run `agy` and sign in.", + }; + return getStatus(launch); + }, + async getCatalogCacheKey({ launch }) { + if (!launch) return undefined; + const env = Object.entries(launch.env).sort(([left], [right]) => left.localeCompare(right)); + return createHash("sha256") + .update(JSON.stringify([launch.command, launch.args, env])) + .digest("hex"); + }, + async connect(request) { + if (!request.versions.includes(1)) + throw new AntigravityError("Provider protocol version 1 is required"); + if (!request.launch) + throw new AntigravityError("The daemon must supply the Antigravity launch"); + const negotiated = negotiateProviderCapabilities(request.capabilities, capabilities); + return createConnection(request.launch, negotiated); + }, + }; +} + +function createConnection( + launch: ProviderLaunch, + negotiated: readonly string[], +): ProviderConnection { + const sessions = new Map(); + const listeners = new Set<(event: ProviderEvent) => void>(); + let discovered: ProviderCatalog | null = null; + let closed = false; + let pending = Promise.resolve(); + + function emit(event: ProviderEvent): void { + if (closed) return; + for (const listener of listeners) listener(event); + } + function catalog(): ProviderCatalog { + if (discovered === null) + throw new AntigravityError("Antigravity catalog has not been discovered"); + return discovered; + } + function session(id: string): Session { + const found = sessions.get(id); + if (!found) throw new AntigravityError(`Unknown Antigravity session: ${id}`); + return found; + } + async function dispatch(input: ProviderInput): Promise { + switch (input.type) { + case "catalog": + discovered = await getCatalog(launch, input.cwd); + emit({ + type: "catalog", + requestId: input.requestId, + catalog: catalog(), + }); + return; + case "session.open": { + if (sessions.has(input.sessionId)) + throw new AntigravityError(`Session already exists: ${input.sessionId}`); + if (discovered === null) discovered = await getCatalog(launch, input.config.cwd); + const opened = new Session({ + id: input.sessionId, + config: input.config, + persistence: input.persistence, + catalog, + launch, + emit, + }); + sessions.set(input.sessionId, opened); + try { + await opened.open(input.requestId, negotiated); + } catch (error) { + sessions.delete(input.sessionId); + await opened.close(); + throw error; + } + return; + } + case "session.prompt": + await session(input.sessionId).prompt(input.prompt); + return; + case "session.configure": + session(input.sessionId).configure(input.changes); + break; + case "session.interrupt": + await session(input.sessionId).interrupt(); + break; + case "session.close": { + await session(input.sessionId).close(); + sessions.delete(input.sessionId); + emit({ type: "session.closed", sessionId: input.sessionId }); + break; + } + default: + throw new AntigravityError(`Unsupported Antigravity operation: ${input.type}`); + } + emit({ type: "request.completed", requestId: input.requestId }); + } + function failed(input: ProviderInput, error: unknown): void { + const message = error instanceof Error ? error.message : String(error); + const code = error instanceof AntigravityError ? error.code : "ANTIGRAVITY_ERROR"; + if (input.type === "session.prompt") + emit({ + type: "session.prompt_result", + sessionId: input.sessionId, + clientMessageId: input.prompt.clientMessageId, + result: { type: "failed", error: { message, code } }, + }); + else if ("requestId" in input) + emit({ type: "request.failed", requestId: input.requestId, error: { message, code } }); + } + return { + version: 1, + capabilities: negotiated, + async send(input) { + if (closed) throw new AntigravityError("Antigravity connection is closed"); + requireProviderCapabilities(negotiated, input); + pending = pending + .then(async () => { + if (!closed) await dispatch(input); + return undefined; + }) + .catch((error) => failed(input, error)); + }, + onEvent(listener) { + listeners.add(listener); + return () => listeners.delete(listener); + }, + async close() { + if (closed) return; + closed = true; + await pending; + await Promise.all([...sessions.values()].map((opened) => opened.close())); + sessions.clear(); + listeners.clear(); + }, + }; +} diff --git a/plugins/antigravity-provider/test/agy.mjs b/plugins/antigravity-provider/test/agy.mjs new file mode 100644 index 0000000000..91b0531b56 --- /dev/null +++ b/plugins/antigravity-provider/test/agy.mjs @@ -0,0 +1,102 @@ +import { readFileSync, appendFileSync } from "node:fs"; +import { createInterface } from "node:readline"; + +const args = process.argv.slice(2); +const log = process.env.AGY_TEST_LOG; +function record(value) { + if (log) appendFileSync(log, `${JSON.stringify(value)}\n`); +} +record({ args, pid: process.pid, env: process.env.AGY_SESSION_VALUE }); +function fixture(name, stream = "stdout") { + return readFileSync(new URL(`./fixtures/${name}.${stream}.ndjson`, import.meta.url), "utf8") + .trim() + .split("\n") + .filter(Boolean) + .map((line) => JSON.parse(line).data); +} +if (args.includes("--version")) { + process.stdout.write(`${process.env.AGY_TEST_VERSION || "1.2.13"}\n`); + process.exit(0); +} +if (args.includes("models")) { + if (process.env.AGY_TEST_AUTH === "missing") { + process.stderr.write(fixture("models-unauthenticated", "stderr").join("\n")); + process.exit(1); + } + process.stdout.write(fixture("models-api-key").join("\n") + "\n"); + process.exit(0); +} +if (process.env.AGY_TEST_STARTUP === "auth") { + process.stderr.write(fixture("not-signed-in", "stderr").join("\n")); + process.stdout.write(JSON.stringify(fixture("not-signed-in")[0]) + "\n"); + process.exit(1); +} +if (process.env.AGY_TEST_STARTUP === "exit") process.exit(3); +if (process.env.AGY_TEST_STARTUP === "stderr") { + process.stderr.write("Error: authentication required. Run 'agy' to log in, then retry.\n"); + process.exit(1); +} +const resumeIndex = args.indexOf("--conversation"); +const conversationId = + resumeIndex >= 0 ? args[resumeIndex + 1] : fixture("text-turn")[0].conversation_id; +let denialResults = 0; +function emit(frame) { + const converted = structuredClone(frame); + if (converted.conversation_id !== undefined) converted.conversation_id = conversationId; + if (converted.step_update) converted.step_update.conversation_id = conversationId; + if (converted.result) { + converted.result.conversation_id = conversationId; + if (process.env.AGY_TEST_DENIAL_COUNTS && converted.result.denied_actions?.length) { + converted.result.denied_actions = Array.from( + { length: Number(process.env.AGY_TEST_DENIAL_COUNTS.split(",")[denialResults++]) }, + () => converted.result.denied_actions[0], + ); + } + } + process.stdout.write(JSON.stringify(converted) + "\n"); +} +const init = fixture("text-turn")[0]; +if (args.includes("--model")) init.init.model = args[args.indexOf("--model") + 1]; +emit(init); +let interrupted = null; +const turns = new Map(); +const lines = createInterface({ input: process.stdin }); +lines.on("line", (line) => { + const input = JSON.parse(line); + record({ input }); + const text = input.message.content; + let name = "text-turn"; + if (text.includes("Attached image: ")) name = "image-file-external"; + if (text.includes("TOOLS")) name = "tools-default"; + if (text.includes("DENIAL_REPLAY")) name = "denial-replay"; + if (text.includes("SUBAGENT")) name = "subagent"; + if (text.includes("MULTI")) name = "multi-turn-reasoning"; + if (text.includes("RESUME")) name = "resume"; + if (text.includes("HANG")) name = "sigint-mid-text"; + if (text.includes("TOOL_HANG")) name = "sigint-mid-tool"; + const frames = fixture(name).slice(1); + if (name.startsWith("sigint")) { + interrupted = frames[frames.length - 1]; + for (const frame of frames.slice(0, -1)) emit(frame); + return; + } + const count = frames.filter((frame) => frame.event === "result").length; + const ordinal = (turns.get(name) || 0) % count; + turns.set(name, ordinal + 1); + let resultCount = 0; + for (const frame of frames) { + if (resultCount === ordinal) emit(frame); + if (frame.event === "result") resultCount += 1; + } +}); +process.on("SIGINT", () => { + record({ signal: "SIGINT" }); + if (process.env.AGY_TEST_IGNORE_SIGNALS === "yes") return; + if (interrupted) emit(interrupted); + process.exit(1); +}); +process.on("SIGTERM", () => { + record({ signal: "SIGTERM" }); + if (process.env.AGY_TEST_IGNORE_SIGNALS !== "yes") process.exit(1); +}); +lines.on("close", () => process.exit(0)); diff --git a/plugins/antigravity-provider/test/fixtures/README.md b/plugins/antigravity-provider/test/fixtures/README.md new file mode 100644 index 0000000000..b4b1ef264f --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/README.md @@ -0,0 +1,100 @@ +# agy 1.2.13 stream-json captures + +`denial-replay.stdout.ndjson` replays two turns from one real 1.2.13 process: a denied shell +command, then a text-only answer whose result retains the first denial. The replay clock is +synthetic; frames retain the native cumulative denial payload. + +Captured on Linux x86_64 on 2026-09-29. These are observations from the supplied binary, not guarantees for the Google sign-in route. No product code or capture drivers are included. + +Each row names a capture prefix. Its `.stdin.ndjson`, `.stdout.ndjson`, `.stderr.ndjson`, and `.capture.ndjson` companions keep the streams separate; empty streams are empty files. Every line wraps the exact decoded JSON frame or text line in `{t_ms, stream, data}`. `t_ms` is milliseconds since construction immediately before spawn. Capture annotations record launch arguments, configuration, signals, EOF, timeouts, and exit code; they are not CLI output. Line references refer to the indicated file, starting at 1. + +Commands below run from ``. `` expands to `/.local/opt/agy/1.2.13/agy` in the real installation home; the child uses a separate isolated `` for state. All captures remove inherited credential/provider environment variables, set `HOME`, `XDG_CONFIG_HOME=/.config`, `XDG_CACHE_HOME=/.cache`, `XDG_DATA_HOME=/.local/share`, `BROWSER=/bin/true`, and loopback-only `NO_PROXY`/`no_proxy`. Neither PATH nor shell rc files changed. Resume rows reuse the original isolated HOME. + +For rows marked **API**, the environment also contains `GEMINI_API_KEY=` and `GOOGLE_GEMINI_BASE_URL=http://127.0.0.1:`, and `/.gemini/antigravity-cli/settings.json` contains `{"modelProvider":"gemini"}`. **Env only** sets those two variables without the setting; **None** has neither. The loopback translator reused the prior QA route and forwarded real requests to a paid upstream model. Its credential was read programmatically inside the temporary proxy, with later captures passing it through the proxy environment. It was never passed to agy or recorded. The translator maps all requested Gemini models to one upstream Gemini model: request routing and CLI selection are verified, native model identity/quality is not. + +The translator emits text, function calls, thought parts and usage. Its usage mapping does not populate Gemini thinking/cache counts, so observed zero `thinking_tokens`/`cache_read_tokens` does not prove native zeros. No dedicated reasoning frames appeared, including the matched high-effort probe. + +``, ``, ``, ``, ``, and `` are redactions. Unrelated project/model names and help examples are redacted without removing frames. Delta chunk boundaries remain; a path split across deltas can have a different placeholder spelling than the complete result. UUIDs are retained for conversation correlation. + +| Fixture prefix | Exact argv (redacted) | Auth | Demonstrates | agy | +| --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | +| `agents` | ` agents` | API | Available agents listing is plain text. | 1.2.13 | +| `api-key-env-only` | ` --input-format stream-json --output-format stream-json` | Env only | Dummy key and custom endpoint alone do not select API-key auth. | 1.2.13 | +| `api-key-settings` | ` --input-format stream-json --output-format stream-json` | API | modelProvider=gemini yields init; missing event input is rejected. | 1.2.13 | +| `cwd-without-add-dir` | ` --input-format stream-json --output-format stream-json --dangerously-skip-permissions` | API | Without add-dir, model-issued pwd returned launch cwd; required tool Cwd is not exposed by this wire frame. | 1.2.13 | +| `effort-unsupported-family` | ` --input-format stream-json --output-format stream-json --model '' --effort high` | API | Unsupported non-Gemini family rejects effort; model reference redacted. | 1.2.13 | +| `effort-encoded-match` | ` --input-format stream-json --output-format stream-json --model gemini-3.8-flash-high --effort high` | API | Matching high effort accepted; arithmetic turn; no thought delta observed. | 1.2.13 | +| `effort-encoded-mismatch` | ` --input-format stream-json --output-format stream-json --model gemini-3.8-flash-high --effort low` | API | High-suffixed model conflicts with effort=low; no init. | 1.2.13 | +| `effort-mismatch` | ` --input-format stream-json --output-format stream-json --model gemini-3.1-pro-preview --effort low` | API | Unlisted preview ID does not support effort; startup ERROR. | 1.2.13 | +| `help` | ` --help` | None | Launch flags and subcommands; help is on stderr. | 1.2.13 | +| `image-content` | ` --input-format stream-json --output-format stream-json --disable-slash-commands` | API | Image source block rejected: only text blocks supported. | 1.2.13 | +| `mcp-add-http` | ` mcp add phase0 'http://127.0.0.1:/mcp'` | None | Writes .gemini/config/mcp_config.json; file content recorded in capture annotations. | 1.2.13 | +| `mcp-add` | ` mcp add phase0 --url 'http://127.0.0.1:/mcp'` | None | Invalid --url syntax rejected; no configuration written. | 1.2.13 | +| `mcp-global` | ` --input-format stream-json --output-format stream-json --add-dir '' --disable-slash-commands --dangerously-skip-permissions` | API | Global HTTP server discovered; strict tool call fails because arguments is a string. | 1.2.13 | +| `mcp-help` | ` mcp add --help` | None | MCP add syntax, HTTP transport, headers/env; flags precede name. | 1.2.13 | +| `mcp-list` | ` mcp list` | None | Model-free empty server list, exit 0. | 1.2.13 | +| `mcp-project-config` | ` --input-format stream-json --output-format stream-json --add-dir '' --disable-slash-commands --dangerously-skip-permissions` | API | Workspace .gemini/config.json mcpServers not discovered in this probe. | 1.2.13 | +| `mcp-project-isolated` | ` --input-format stream-json --output-format stream-json --add-dir '' --dangerously-skip-permissions` | API | Project MCP discovery; no global MCP config before/after; strict arguments error. | 1.2.13 | +| `mcp-project` | ` --input-format stream-json --output-format stream-json --add-dir '' --disable-slash-commands --dangerously-skip-permissions` | API | Project .agents/mcp_config.json discovers HTTP server; strict call rejects string arguments; later translator 502 retained. | 1.2.13 | +| `mcp-second-add-dir` | ` --input-format stream-json --output-format stream-json --disable-slash-commands --dangerously-skip-permissions --add-dir '' --add-dir '/owned'` | API | Discovers project MCP from the owned directory; strict arguments fail. Shell uses owned directory and writes occur in both directories. | 1.2.13 | +| `mcp-owned-first-add-dir` | ` --input-format stream-json --output-format stream-json --disable-slash-commands --dangerously-skip-permissions --add-dir '/owned' --add-dir ''` | API | Reversing argv does not restore command working directory; owned directory remains the shell default. | 1.2.13 | +| `mcp-owned-cwd` | ` --input-format stream-json --output-format stream-json --disable-slash-commands --dangerously-skip-permissions --add-dir ''`, launched in `/owned` through a symlink | API | Discovers owned project MCP, but shell and initial read resolve against owned cwd; writes occur in both directories. | 1.2.13 | +| `mcp-agent-file` | ` --input-format stream-json --output-format stream-json --disable-slash-commands --dangerously-skip-permissions --add-dir '' --agent '/owned/agent.md'` | API | Direct external agent filename falls back to default; no MCP discovery. Shell/read/write use workspace. | 1.2.13 | +| `mcp-agent-definition` | ` --input-format stream-json --output-format stream-json --add-dir '/owned' --add-dir '' --agent phase2 --dangerously-skip-permissions` | API | Nested Markdown definition with inline mcpServers is not found; default agent, no MCP server. | 1.2.13 | +| `mcp-agent-discovered` | ` --input-format stream-json --output-format stream-json --add-dir '/owned' --add-dir '' --agent phase2 --dangerously-skip-permissions` | API | Flat Markdown definition plus agents.json manifest is also not found; default agent, no MCP server. | 1.2.13 | +| `image-file-external` | ` --input-format stream-json --output-format stream-json --add-dir '' --disable-slash-commands` | API | Default mode reads an external image path with view_file and correctly describes its pixels, without an extra add-dir. | 1.2.13 | +| `model-switch-invalid` | ` --input-format stream-json --output-format stream-json --add-dir '' --conversation b966b16a-caf4-448a-bb24-0e34956022b0 --model gemini-3.1-flash-lite-preview` | API | Unlisted model ID rejected before init. | 1.2.13 | +| `model-switch-network` | ` --input-format stream-json --output-format stream-json --conversation b966b16a-caf4-448a-bb24-0e34956022b0 --model gemini-3.8-flash-low` | API | Listed model respawn plus actual Gemini request path in capture annotations; COBALT preserved. | 1.2.13 | +| `model-switch-resume` | ` --input-format stream-json --output-format stream-json --add-dir '' --conversation b966b16a-caf4-448a-bb24-0e34956022b0 --model gemini-3.1-flash-high` | API | Another unlisted model ID rejected before init. | 1.2.13 | +| `model-switch-valid` | ` --input-format stream-json --output-format stream-json --conversation b966b16a-caf4-448a-bb24-0e34956022b0 --model gemini-3.8-flash-low` | API | Listed low-effort model accepted on same conversation; COBALT preserved. | 1.2.13 | +| `models-api-key` | ` models` | API | Authenticated catalog is tab-separated model ID and display label. | 1.2.13 | +| `models-unauthenticated` | ` models` | None | Models requires auth; no catalog on stdout. | 1.2.13 | +| `multi-turn-reasoning` | ` --input-format stream-json --output-format stream-json --add-dir '' --disable-slash-commands` | API | Two text turns with arithmetic; per-generation and cumulative usage. No thought delta observed. | 1.2.13 | +| `not-signed-in` | ` --input-format stream-json --output-format stream-json` | None | No credentials: result ERROR, no init, exit 1. | 1.2.13 | +| `project-skill` | ` --input-format stream-json --output-format stream-json --add-dir '' --disable-slash-commands` | API | Project skill read via view_file; marker reply. No distinct skill event. | 1.2.13 | +| `question-auto-denial` | ` --input-format stream-json --output-format stream-json --add-dir ''` | API | Requested ask_question becomes unknown DONE step, auto-skipped; no question payload on wire. | 1.2.13 | +| `resume-after-interrupt` | ` --input-format stream-json --output-format stream-json --conversation a2c2132f-0f10-400b-a142-74f82230f449` | API | Same interrupted conversation resumes and accepts another turn. | 1.2.13 | +| `resume` | ` --input-format stream-json --output-format stream-json --add-dir '' --disable-slash-commands --conversation b966b16a-caf4-448a-bb24-0e34956022b0` | API | Remembered COBALT after respawn; no old steps replayed. | 1.2.13 | +| `schema-prompt` | ` --input-format stream-json --output-format stream-json` | API | Unknown prompt event ignored with stderr warning; no turn. | 1.2.13 | +| `schema-user-message` | ` --input-format stream-json --output-format stream-json` | API | Decoding a string message fails before event dispatch. | 1.2.13 | +| `schema-user` | ` --input-format stream-json --output-format stream-json` | API | String message rejected; message must be an object. | 1.2.13 | +| `sigint-mid-text` | ` --input-format stream-json --output-format stream-json --add-dir '' --disable-slash-commands --dangerously-skip-permissions` | API | SIGINT after first text delta: interrupted result and exit 1. | 1.2.13 | +| `sigint-mid-tool` | ` --input-format stream-json --output-format stream-json --add-dir '' --dangerously-skip-permissions` | API | SIGINT while run_command ACTIVE: interrupted result and exit 1. | 1.2.13 | +| `sigterm` | ` --input-format stream-json --output-format stream-json --add-dir '' --disable-slash-commands --dangerously-skip-permissions` | API | SIGTERM after user_input: interrupted result and exit 1. | 1.2.13 | +| `slash-disabled` | ` --input-format stream-json --output-format stream-json --disable-slash-commands` | API | /help sent literally to model; normal turn with built-in skill reads. | 1.2.13 | +| `slash-enabled` | ` --input-format stream-json --output-format stream-json` | API | /help rejected in driver mode; result ERROR and exit 2. | 1.2.13 | +| `stdin-close-mid-turn` | ` --input-format stream-json --output-format stream-json --add-dir '' --disable-slash-commands --dangerously-skip-permissions` | API | EOF after user_input: active turn finishes successfully, exit 0. | 1.2.13 | +| `stdin-mid-turn` | ` --input-format stream-json --output-format stream-json --add-dir '' --disable-slash-commands --dangerously-skip-permissions` | API | Second prompt after first user_input queues a separate turn; two results. | 1.2.13 | +| `subagent` | ` --input-format stream-json --output-format stream-json --add-dir '' --dangerously-skip-permissions` | API | Define/invoke child; subagent_info contains child ID/log URI/workspaces; child reply reaches parent. | 1.2.13 | +| `text-block` | ` --input-format stream-json --output-format stream-json --add-dir ''` | API | Array of text content blocks accepted. | 1.2.13 | +| `text-turn` | ` --input-format stream-json --output-format stream-json --add-dir ''` | API | event=user, message.content string; streaming text and final usage. | 1.2.13 | +| `tools-accept-edits` | ` --input-format stream-json --output-format stream-json --add-dir '' --disable-slash-commands --mode accept-edits` | API | Read, create, edit allowed; shell auto-denied. Result SUCCESS includes denied_actions. No permission request. | 1.2.13 | +| `tools-default` | ` --input-format stream-json --output-format stream-json --add-dir '' --disable-slash-commands` | API | Read, create, edit allowed; shell auto-denied. Result SUCCESS includes denied_actions. No permission request. | 1.2.13 | +| `tools-plan` | ` --input-format stream-json --output-format stream-json --add-dir '' --disable-slash-commands --mode plan` | API | Read, create, edit allowed; shell auto-denied. Result SUCCESS includes denied_actions. No permission request. | 1.2.13 | +| `tools-sandbox` | ` --input-format stream-json --output-format stream-json --add-dir '' --disable-slash-commands --sandbox` | API | Read, create, edit allowed; shell auto-denied. Result SUCCESS includes denied_actions. No permission request. | 1.2.13 | +| `tools-skip-permissions` | ` --input-format stream-json --output-format stream-json --add-dir '' --disable-slash-commands --dangerously-skip-permissions` | API | Read, create, edit and shell succeed; permission_mode=always-proceed. | 1.2.13 | +| `version` | ` --version` | None | Installed binary reports 1.2.13. | 1.2.13 | +| `mcp-http.requests.ndjson` | `node /mcp-server.mjs` alongside the MCP argv above | None | Actual HTTP JSON-RPC requests to a strict SDK server; `params.arguments` is a string. Server implements initialize, tools/list, and echo. | 1.2.13 client | +| `state-inspection.ndjson` | `HOME= strace -f -e trace=file -o /files.trace --input-format stream-json --output-format stream-json`; `python3 /finalize.py`; `firecrawl scrape --format markdown` | None | Binary checksum, isolated state accesses, read-only SQLite listings, written file contents, and manifest retrieval caveat. Inspection data, not agy stdout. | 1.2.13 | + +`project-skill` used `/.agents/skills/phase0-marker/SKILL.md` with name `phase0-marker`, description “Return the Phase 0 marker when requested,” and instructions “Respond with PHASE0_SKILL_MARKER. No tools are needed after reading this skill.” + +Each tools capture begins with `input.txt` containing `PHASE0_READ_MARKER`, then requests create→replace from `PHASE0_WRITE` to `PHASE0_EDIT`, then `printf PHASE0_SHELL`. `state-inspection.ndjson` records the resulting files. These successful writes disprove an enforced read-only boundary for default/plan on this credential route. `--sandbox` alone still denies the shell at permission policy, so OS sandbox enforcement is unverified. + +## MCP discovery + +Phase 0 project setup (`mcp-project` and `mcp-project-isolated`) places `{"mcpServers":{"phase0":{"serverUrl":"http://127.0.0.1:/mcp"}}}` in `/.agents/mcp_config.json`. `mcp-project-config` instead uses `/.gemini/config.json`; that file did not expose the server. HTTP calls send string arguments and fail strict validation. + +Phase 2 places the same HTTP configuration, named `phase2`, in a plugin-owned `/owned/.agents/mcp_config.json`. Both add-dir orders discover it, as does launching in the owned directory and adding the real workspace. All three turns execute shell `pwd` in the owned directory. The file turns read the workspace marker but also write `output.txt` into the owned directory. These routes fail the requirement that tools resolve against the user's workspace. Reordering arguments did not change the observed shell cwd; the precise directory selection rule is unverified. + +The external `--agent` file, nested agent definition, and flat definition with an `agents.json` manifest contain inline `mcpServers`. Native logs record “not found, falling back to default” for each. These experiments do not establish that every agent definition format is unsupported. `parity-inspection.ndjson` records their definitions and log evidence. Searching the tested binary's strings found no `AGY_MCP`, `MCP_CONFIG`, `AGY_CONFIG`, or `ANTIGRAVITY_CONFIG` override; no environment discovery route was verified. + +No tested MCP route satisfies both discovery and workspace ownership. MCP implementation stopped at this gate: no relay or MCP capability change was added. Images and the daemon e2e continued independently. No global MCP configuration exists in the isolated homes, and no configuration was written into the user repository. + +## Image file fallback + +`image-file-external.png` contains a red triangle on the left and a blue circle on the lower right, against white. The prompt supplies only its path and asks for visible shapes and colors. `image-file-external.stdout.ndjson:4–5` records a successful `view_file`; the result describes the pixels correctly. The native function response supplies bytes at `functionResponse.parts[].inlineData`. The temporary translator was corrected to forward these nested parts to the model; forwarding top-level parts alone dropped the image. `parity-inspection.ndjson` records the image hash and response-part location. + +This verifies one default-mode CLI turn reading a file outside the workspace without an extra add-dir. A later isolated-daemon run attached the same image through the provider and returned the same shapes and colors after a successful `view_file`. The provider advertises `prompt.image`; MCP remains outside the parity implementation. + +Unverified: Google login behaviour; native reasoning/cached-token accounting; filesystem/network isolation under sandbox plus approved commands; exhaustive tool policy; other agent definition formats or environment MCP overrides; child transcript streaming beyond parent metadata; a stable public conversation-list command. No fresh login was created. The fetched official manifest returned 1.2.12 while this binary reports 1.2.13; the checksum identifies the tested binary, but that response does not establish its release provenance. diff --git a/plugins/antigravity-provider/test/fixtures/agents.capture.ndjson b/plugins/antigravity-provider/test/fixtures/agents.capture.ndjson new file mode 100644 index 0000000000..cc01e91070 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/agents.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "agents"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 309, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/agents.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/agents.stderr.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/agents.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/agents.stdin.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/agents.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/agents.stdout.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/api-key-env-only.capture.ndjson b/plugins/antigravity-provider/test/fixtures/api-key-env-only.capture.ndjson new file mode 100644 index 0000000000..9c7a770416 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/api-key-env-only.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json"], "cwd": "", "home": "", "settings": null, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 452, "stream": "capture", "data": {"exit_code": 1}} diff --git a/plugins/antigravity-provider/test/fixtures/api-key-env-only.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/api-key-env-only.stderr.ndjson new file mode 100644 index 0000000000..d315088707 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/api-key-env-only.stderr.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 288, "stream": "stderr", "data": "Error: authentication required. Run 'agy' to log in, then retry."} +{"t_ms": 288, "stream": "stderr", "data": "error: authentication failed or timed out"} diff --git a/plugins/antigravity-provider/test/fixtures/api-key-env-only.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/api-key-env-only.stdin.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/api-key-env-only.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/api-key-env-only.stdout.ndjson new file mode 100644 index 0000000000..60cc38245f --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/api-key-env-only.stdout.ndjson @@ -0,0 +1 @@ +{"t_ms": 288, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "", "status": "ERROR", "response": "", "error": "authentication failed or timed out", "duration_seconds": 0, "num_turns": 0, "usage": {"input_tokens": 0, "output_tokens": 0, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 0}}}} diff --git a/plugins/antigravity-provider/test/fixtures/api-key-settings.capture.ndjson b/plugins/antigravity-provider/test/fixtures/api-key-settings.capture.ndjson new file mode 100644 index 0000000000..1c03483e72 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/api-key-settings.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 525, "stream": "capture", "data": {"exit_code": 1}} diff --git a/plugins/antigravity-provider/test/fixtures/api-key-settings.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/api-key-settings.stderr.ndjson new file mode 100644 index 0000000000..1c47437a40 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/api-key-settings.stderr.ndjson @@ -0,0 +1 @@ +{"t_ms": 374, "stream": "stderr", "data": "error: stream input message is missing the \"event\" field"} diff --git a/plugins/antigravity-provider/test/fixtures/api-key-settings.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/api-key-settings.stdin.ndjson new file mode 100644 index 0000000000..97362f53ed --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/api-key-settings.stdin.ndjson @@ -0,0 +1 @@ +{"t_ms": 373, "stream": "stdin", "data": {"type": "user", "message": {"role": "user", "content": "Reply with PHASE0_HELLO. No tools."}}} diff --git a/plugins/antigravity-provider/test/fixtures/api-key-settings.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/api-key-settings.stdout.ndjson new file mode 100644 index 0000000000..567b1fc1f1 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/api-key-settings.stdout.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 373, "stream": "stdout", "data": {"event": "init", "conversation_id": "e629b41a-decf-4212-a9fb-3809e8f21122", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "request-review"}}} +{"t_ms": 374, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "e629b41a-decf-4212-a9fb-3809e8f21122", "status": "ERROR", "response": "", "error": "stream input message is missing the \"event\" field", "duration_seconds": 0, "num_turns": 0, "usage": {"input_tokens": 0, "output_tokens": 0, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 0}}}} diff --git a/plugins/antigravity-provider/test/fixtures/cwd-without-add-dir.capture.ndjson b/plugins/antigravity-provider/test/fixtures/cwd-without-add-dir.capture.ndjson new file mode 100644 index 0000000000..78eedeec6f --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/cwd-without-add-dir.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--dangerously-skip-permissions"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 5003, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/cwd-without-add-dir.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/cwd-without-add-dir.stderr.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/cwd-without-add-dir.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/cwd-without-add-dir.stdin.ndjson new file mode 100644 index 0000000000..52514739aa --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/cwd-without-add-dir.stdin.ndjson @@ -0,0 +1 @@ +{"t_ms": 318, "stream": "stdin", "data": {"event": "user", "message": {"content": "Use run_command to run pwd, WaitMsBeforeAsync 1000. Report the output."}}} diff --git a/plugins/antigravity-provider/test/fixtures/cwd-without-add-dir.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/cwd-without-add-dir.stdout.ndjson new file mode 100644 index 0000000000..469cdadcca --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/cwd-without-add-dir.stdout.ndjson @@ -0,0 +1,11 @@ +{"t_ms": 318, "stream": "stdout", "data": {"event": "init", "conversation_id": "3cea6151-badf-4b4a-ad58-bf3c0b02db25", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "always-proceed"}}} +{"t_ms": 347, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "3cea6151-badf-4b4a-ad58-bf3c0b02db25", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 3066, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "3cea6151-badf-4b4a-ad58-bf3c0b02db25", "step_index": 1, "state": "DONE", "step_type": "agent_response", "duration_seconds": 2.698205516, "usage": {"input_tokens": 11255, "output_tokens": 138, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11393}}}} +{"t_ms": 3066, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "3cea6151-badf-4b4a-ad58-bf3c0b02db25", "step_index": 2, "state": "ACTIVE", "step_type": "tool", "tool_name": "run_command", "tool_info": {"name": "run_command", "parameters": {"CommandLine": "pwd"}}}}} +{"t_ms": 3066, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "3cea6151-badf-4b4a-ad58-bf3c0b02db25", "step_index": 2, "state": "DONE", "step_type": "tool", "tool_name": "run_command", "duration_seconds": 0.006078385, "tool_info": {"name": "run_command", "parameters": {"CommandLine": "pwd"}, "output": "\r\n"}}}} +{"t_ms": 4626, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "3cea6151-badf-4b4a-ad58-bf3c0b02db25", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "The output"}}} +{"t_ms": 4678, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "3cea6151-badf-4b4a-ad58-bf3c0b02db25", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " of running "}}} +{"t_ms": 4704, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "3cea6151-badf-4b4a-ad58-bf3c0b02db25", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "`pwd` is:\n\n```text\n/works"}}} +{"t_ms": 4729, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "3cea6151-badf-4b4a-ad58-bf3c0b02db25", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "pace\n```"}}} +{"t_ms": 4881, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "3cea6151-badf-4b4a-ad58-bf3c0b02db25", "step_index": 3, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 1.824928838, "usage": {"input_tokens": 11382, "output_tokens": 24, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11406}}}} +{"t_ms": 4881, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "3cea6151-badf-4b4a-ad58-bf3c0b02db25", "status": "SUCCESS", "response": "The output of running `pwd` is:\n\n```text\n\n```\n", "duration_seconds": 4.555206291, "num_turns": 1, "usage": {"input_tokens": 22637, "output_tokens": 162, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 22799}}}} diff --git a/plugins/antigravity-provider/test/fixtures/denial-replay.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/denial-replay.stdout.ndjson new file mode 100644 index 0000000000..b667b505cc --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/denial-replay.stdout.ndjson @@ -0,0 +1,11 @@ +{"t_ms":0,"stream":"stdout","data":{"event":"init","conversation_id":"e8243164-64d5-4419-9dbb-c0733300b557","init":{"cwd":"/tmp/agy-p3/workspace","tools":["ask_custom_permission","ask_permission","ask_question","browser_click_element","browser_drag_pixel_to_pixel","browser_get_dom","browser_get_network_request","browser_input","browser_list_network_requests","browser_mouse_down","browser_mouse_up","browser_move_mouse","browser_press_key","browser_refresh_page","browser_resize_window","browser_scroll","browser_scroll_dom","browser_select_option","browser_subagent","call_mcp_tool","capture_browser_console_logs","capture_browser_screenshot","click_browser_pixel","command_status","define_subagent","delete_knowledge","execute_browser_javascript","find_by_name","finish","generate_image","grep_search","invoke_subagent","list_browser_pages","list_dir","list_permissions","list_resources","manage_inbox","manage_subagents","manage_task","multi_replace_file_content","notebook_edit","notebook_execution","open_browser_url","read_browser_page","read_resource","read_url_content","replace_file_content","run_command","schedule","search_web","sed_file","send_command_input","send_message","view_file","wait","wait_5_seconds","write_to_file"],"permission_mode":"request-review"}}} +{"t_ms":1,"stream":"stdout","data":{"event":"step_update","step_update":{"conversation_id":"e8243164-64d5-4419-9dbb-c0733300b557","step_index":0,"state":"DONE","step_type":"user_input"}}} +{"t_ms":2,"stream":"stdout","data":{"event":"step_update","step_update":{"conversation_id":"e8243164-64d5-4419-9dbb-c0733300b557","step_index":1,"state":"DONE","step_type":"agent_response","duration_seconds":2.86246236,"usage":{"input_tokens":11252,"output_tokens":163,"thinking_tokens":0,"cache_read_tokens":0,"total_tokens":11415}}}} +{"t_ms":3,"stream":"stdout","data":{"event":"step_update","step_update":{"conversation_id":"e8243164-64d5-4419-9dbb-c0733300b557","step_index":2,"state":"ACTIVE","step_type":"tool","tool_name":"run_command","tool_info":{"name":"run_command","parameters":{"CommandLine":"printf AGY_DENIAL_PROBE"}}}}} +{"t_ms":4,"stream":"stdout","data":{"event":"step_update","step_update":{"conversation_id":"e8243164-64d5-4419-9dbb-c0733300b557","step_index":2,"state":"ERROR","step_type":"tool","tool_name":"run_command","duration_seconds":0.002264915,"tool_info":{"name":"run_command","parameters":{"CommandLine":"printf AGY_DENIAL_PROBE"},"error":{"type":"TOOL_ERROR","message":"permission check failed for command \"printf AGY_DENIAL_PROBE\": user denied permission to run command:\nprintf AGY_DENIAL_PROBE\nDo not attempt to circumvent this denial by rephrasing the command, using alternative tools/scripts (e.g. python, sh, curl), or accessing the same target resource. Proceed without performing this action."}}}}} +{"t_ms":5,"stream":"stdout","data":{"event":"result","result":{"conversation_id":"e8243164-64d5-4419-9dbb-c0733300b557","status":"SUCCESS","response":"","duration_seconds":2.890969963,"num_turns":1,"usage":{"input_tokens":11252,"output_tokens":163,"thinking_tokens":0,"cache_read_tokens":0,"total_tokens":11415},"denied_actions":[{"action":"command","display_name":"RunCommand"}]}}} +{"t_ms":6,"stream":"stdout","data":{"event":"step_update","step_update":{"conversation_id":"e8243164-64d5-4419-9dbb-c0733300b557","step_index":3,"state":"DONE","step_type":"user_input"}}} +{"t_ms":7,"stream":"stdout","data":{"event":"step_update","step_update":{"conversation_id":"e8243164-64d5-4419-9dbb-c0733300b557","step_index":4,"state":"ACTIVE","step_type":"agent_response","text_delta":"FOLLOWUP"}}} +{"t_ms":8,"stream":"stdout","data":{"event":"step_update","step_update":{"conversation_id":"e8243164-64d5-4419-9dbb-c0733300b557","step_index":4,"state":"ACTIVE","step_type":"agent_response","text_delta":"_OK"}}} +{"t_ms":9,"stream":"stdout","data":{"event":"step_update","step_update":{"conversation_id":"e8243164-64d5-4419-9dbb-c0733300b557","step_index":4,"state":"DONE","step_type":"agent_response","text_delta":"\n","duration_seconds":2.11565723,"usage":{"input_tokens":11533,"output_tokens":4,"thinking_tokens":0,"cache_read_tokens":0,"total_tokens":11537}}}} +{"t_ms":10,"stream":"stdout","data":{"event":"result","result":{"conversation_id":"e8243164-64d5-4419-9dbb-c0733300b557","status":"SUCCESS","response":"FOLLOWUP_OK\n","duration_seconds":5.028454184,"num_turns":2,"usage":{"input_tokens":22785,"output_tokens":167,"thinking_tokens":0,"cache_read_tokens":0,"total_tokens":22952},"denied_actions":[{"action":"command","display_name":"RunCommand"}]}}} diff --git a/plugins/antigravity-provider/test/fixtures/effort-encoded-match.capture.ndjson b/plugins/antigravity-provider/test/fixtures/effort-encoded-match.capture.ndjson new file mode 100644 index 0000000000..89227b65a3 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/effort-encoded-match.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--model", "gemini-3.8-flash-high", "--effort", "high"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 4787, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/effort-encoded-match.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/effort-encoded-match.stderr.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/effort-encoded-match.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/effort-encoded-match.stdin.ndjson new file mode 100644 index 0000000000..817c98ad5b --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/effort-encoded-match.stdin.ndjson @@ -0,0 +1 @@ +{"t_ms": 296, "stream": "stdin", "data": {"event": "user", "message": {"content": "Solve 143*127. Think carefully before answering. No tools."}}} diff --git a/plugins/antigravity-provider/test/fixtures/effort-encoded-match.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/effort-encoded-match.stdout.ndjson new file mode 100644 index 0000000000..e56f554df9 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/effort-encoded-match.stdout.ndjson @@ -0,0 +1,19 @@ +{"t_ms": 295, "stream": "stdout", "data": {"event": "init", "conversation_id": "a090b266-2ea9-418c-bb02-c3267268b67b", "init": {"model": "gemini-3.8-flash-high", "cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "request-review"}}} +{"t_ms": 323, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a090b266-2ea9-418c-bb02-c3267268b67b", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 3877, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a090b266-2ea9-418c-bb02-c3267268b67b", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "**18,161**\n\n### Step-by-"}}} +{"t_ms": 3903, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a090b266-2ea9-418c-bb02-c3267268b67b", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "Step Breakdo"}}} +{"t_ms": 3928, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a090b266-2ea9-418c-bb02-c3267268b67b", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "wn:\nUsing the difference"}}} +{"t_ms": 3953, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a090b266-2ea9-418c-bb02-c3267268b67b", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " of squares "}}} +{"t_ms": 3979, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a090b266-2ea9-418c-bb02-c3267268b67b", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "formula $("}}} +{"t_ms": 4055, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a090b266-2ea9-418c-bb02-c3267268b67b", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "a + b)(a - b) = a^2 - b^2$:\n- $143 ="}}} +{"t_ms": 4081, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a090b266-2ea9-418c-bb02-c3267268b67b", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " 135"}}} +{"t_ms": 4132, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a090b266-2ea9-418c-bb02-c3267268b67b", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " + 8$\n- $127"}}} +{"t_ms": 4157, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a090b266-2ea9-418c-bb02-c3267268b67b", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " = 135 - 8$\n\n$$143"}}} +{"t_ms": 4259, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a090b266-2ea9-418c-bb02-c3267268b67b", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " \\times 127 = 135^2 - 8^"}}} +{"t_ms": 4284, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a090b266-2ea9-418c-bb02-c3267268b67b", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "2$$\n$$135"}}} +{"t_ms": 4385, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a090b266-2ea9-418c-bb02-c3267268b67b", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "^2 = 18,225$$\n$$8^2 = 64"}}} +{"t_ms": 4411, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a090b266-2ea9-418c-bb02-c3267268b67b", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "$$\n$$18,22"}}} +{"t_ms": 4462, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a090b266-2ea9-418c-bb02-c3267268b67b", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "5 - 64 = \\ma"}}} +{"t_ms": 4487, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a090b266-2ea9-418c-bb02-c3267268b67b", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "thbf{18,161}$$"}}} +{"t_ms": 4640, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a090b266-2ea9-418c-bb02-c3267268b67b", "step_index": 1, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 4.300859666, "usage": {"input_tokens": 10982, "output_tokens": 553, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11535}}}} +{"t_ms": 4640, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "a090b266-2ea9-418c-bb02-c3267268b67b", "status": "SUCCESS", "response": "**18,161**\n\n### Step-by-Step Breakdown:\nUsing the difference of squares formula $(a + b)(a - b) = a^2 - b^2$:\n- $143 = 135 + 8$\n- $127 = 135 - 8$\n\n$$143 \\times 127 = 135^2 - 8^2$$\n$$135^2 = 18,225$$\n$$8^2 = 64$$\n$$18,225 - 64 = \\mathbf{18,161}$$\n", "duration_seconds": 4.326461265, "num_turns": 1, "usage": {"input_tokens": 10982, "output_tokens": 553, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11535}}}} diff --git a/plugins/antigravity-provider/test/fixtures/effort-encoded-mismatch.capture.ndjson b/plugins/antigravity-provider/test/fixtures/effort-encoded-mismatch.capture.ndjson new file mode 100644 index 0000000000..22f8817b70 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/effort-encoded-mismatch.capture.ndjson @@ -0,0 +1,3 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--model", "gemini-3.8-flash-high", "--effort", "low"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 5006, "stream": "capture", "data": {"timeout_waiting_for": "init", "seconds": 5}} +{"t_ms": 5006, "stream": "capture", "data": {"exit_code": 1}} diff --git a/plugins/antigravity-provider/test/fixtures/effort-encoded-mismatch.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/effort-encoded-mismatch.stderr.ndjson new file mode 100644 index 0000000000..1807a73321 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/effort-encoded-mismatch.stderr.ndjson @@ -0,0 +1 @@ +{"t_ms": 296, "stream": "stderr", "data": "error: invalid model selection (--model \"gemini-3.8-flash-high\" --effort \"low\"): --model gemini-3.8-flash-high conflicts with --effort=low"} diff --git a/plugins/antigravity-provider/test/fixtures/effort-encoded-mismatch.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/effort-encoded-mismatch.stdin.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/effort-encoded-mismatch.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/effort-encoded-mismatch.stdout.ndjson new file mode 100644 index 0000000000..b9669aaaf4 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/effort-encoded-mismatch.stdout.ndjson @@ -0,0 +1 @@ +{"t_ms": 296, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "", "status": "ERROR", "response": "", "error": "invalid model selection (--model \"gemini-3.8-flash-high\" --effort \"low\"): --model gemini-3.8-flash-high conflicts with --effort=low", "duration_seconds": 0, "num_turns": 0, "usage": {"input_tokens": 0, "output_tokens": 0, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 0}}}} diff --git a/plugins/antigravity-provider/test/fixtures/effort-mismatch.capture.ndjson b/plugins/antigravity-provider/test/fixtures/effort-mismatch.capture.ndjson new file mode 100644 index 0000000000..fbee5c5bc5 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/effort-mismatch.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--model", "gemini-3.1-pro-preview", "--effort", "low"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 415, "stream": "capture", "data": {"exit_code": 1}} diff --git a/plugins/antigravity-provider/test/fixtures/effort-mismatch.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/effort-mismatch.stderr.ndjson new file mode 100644 index 0000000000..6072d28192 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/effort-mismatch.stderr.ndjson @@ -0,0 +1 @@ +{"t_ms": 298, "stream": "stderr", "data": "error: invalid model selection (--model \"gemini-3.1-pro-preview\" --effort \"low\"): --effort is not supported for model \"gemini-3.1-pro-preview\""} diff --git a/plugins/antigravity-provider/test/fixtures/effort-mismatch.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/effort-mismatch.stdin.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/effort-mismatch.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/effort-mismatch.stdout.ndjson new file mode 100644 index 0000000000..c0baa6d698 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/effort-mismatch.stdout.ndjson @@ -0,0 +1 @@ +{"t_ms": 298, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "", "status": "ERROR", "response": "", "error": "invalid model selection (--model \"gemini-3.1-pro-preview\" --effort \"low\"): --effort is not supported for model \"gemini-3.1-pro-preview\"", "duration_seconds": 0, "num_turns": 0, "usage": {"input_tokens": 0, "output_tokens": 0, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 0}}}} diff --git a/plugins/antigravity-provider/test/fixtures/effort-unsupported-family.capture.ndjson b/plugins/antigravity-provider/test/fixtures/effort-unsupported-family.capture.ndjson new file mode 100644 index 0000000000..47eee607d9 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/effort-unsupported-family.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--model", "", "--effort", "high"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 358, "stream": "capture", "data": {"exit_code": 1}} diff --git a/plugins/antigravity-provider/test/fixtures/effort-unsupported-family.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/effort-unsupported-family.stderr.ndjson new file mode 100644 index 0000000000..f19c83a09b --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/effort-unsupported-family.stderr.ndjson @@ -0,0 +1 @@ +{"t_ms": 287, "stream": "stderr", "data": "error: invalid model selection (--model \"\" --effort \"high\"): --effort is not supported for model \"\""} diff --git a/plugins/antigravity-provider/test/fixtures/effort-unsupported-family.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/effort-unsupported-family.stdin.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/effort-unsupported-family.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/effort-unsupported-family.stdout.ndjson new file mode 100644 index 0000000000..b85109a9a9 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/effort-unsupported-family.stdout.ndjson @@ -0,0 +1 @@ +{"t_ms": 287, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "", "status": "ERROR", "response": "", "error": "invalid model selection (--model \"\" --effort \"high\"): --effort is not supported for model \"\"", "duration_seconds": 0, "num_turns": 0, "usage": {"input_tokens": 0, "output_tokens": 0, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 0}}}} diff --git a/plugins/antigravity-provider/test/fixtures/help.capture.ndjson b/plugins/antigravity-provider/test/fixtures/help.capture.ndjson new file mode 100644 index 0000000000..6e88cd1f4f --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/help.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--help"], "cwd": "", "home": "", "settings": null, "env": {}}} +{"t_ms": 209, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/help.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/help.stderr.ndjson new file mode 100644 index 0000000000..3a8e68bf4b --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/help.stderr.ndjson @@ -0,0 +1,39 @@ +{"t_ms": 195, "stream": "stderr", "data": "Usage of agy:"} +{"t_ms": 195, "stream": "stderr", "data": " --add-dir Add a directory to the workspace (repeatable) (default [])"} +{"t_ms": 195, "stream": "stderr", "data": " --agent Agent for the current CLI session"} +{"t_ms": 195, "stream": "stderr", "data": " -c Short alias for --continue"} +{"t_ms": 195, "stream": "stderr", "data": " --continue Continue the most recent conversation"} +{"t_ms": 195, "stream": "stderr", "data": " --conversation Resume a previous conversation by ID"} +{"t_ms": 195, "stream": "stderr", "data": " --dangerously-skip-permissions Auto-approve all tool permission requests without prompting"} +{"t_ms": 195, "stream": "stderr", "data": " --disable-slash-commands Disable slash command and skill expansion in print mode"} +{"t_ms": 195, "stream": "stderr", "data": " --effort Reasoning effort for the current CLI session (low|medium|high|max)"} +{"t_ms": 195, "stream": "stderr", "data": " -i Short alias for --prompt-interactive"} +{"t_ms": 195, "stream": "stderr", "data": " --input-format Input format for print mode (text, stream-json). stream-json reads one NDJSON message per line from stdin and runs a turn for each; it requires --output-format stream-json (default text)"} +{"t_ms": 195, "stream": "stderr", "data": " --json-schema Optional JSON schema string or path to a schema file to enforce structured output (for stream-json, only applicable to the final result)"} +{"t_ms": 195, "stream": "stderr", "data": " --log-file Override CLI log file path"} +{"t_ms": 196, "stream": "stderr", "data": " --mode Set the agent execution mode for this session (accept-edits, plan)"} +{"t_ms": 196, "stream": "stderr", "data": " --model Model for the current CLI session"} +{"t_ms": 196, "stream": "stderr", "data": " --new-project Create a new project for this session"} +{"t_ms": 196, "stream": "stderr", "data": " --output-format Output format for print mode (text, json, stream-json) (default text)"} +{"t_ms": 196, "stream": "stderr", "data": " -p Short alias for --print"} +{"t_ms": 196, "stream": "stderr", "data": " --print Run a single prompt non-interactively and print the response"} +{"t_ms": 196, "stream": "stderr", "data": " --print-timeout Optional time limit for print mode; 0 waits until the turn completes (default 0s)"} +{"t_ms": 196, "stream": "stderr", "data": " --project Project ID or project name for the current CLI session"} +{"t_ms": 196, "stream": "stderr", "data": " --prompt Alias for --print"} +{"t_ms": 196, "stream": "stderr", "data": " --prompt-interactive Run an initial prompt interactively and continue the session"} +{"t_ms": 196, "stream": "stderr", "data": " --remote-control Create a remote connection for the CLI session on start up"} +{"t_ms": 196, "stream": "stderr", "data": " --sandbox Run in a sandbox with terminal restrictions enabled"} +{"t_ms": 196, "stream": "stderr", "data": ""} +{"t_ms": 196, "stream": "stderr", "data": "Available subcommands:"} +{"t_ms": 196, "stream": "stderr", "data": " agent List available agents"} +{"t_ms": 196, "stream": "stderr", "data": " agents List available agents"} +{"t_ms": 196, "stream": "stderr", "data": " changelog Show changelog and release notes"} +{"t_ms": 196, "stream": "stderr", "data": " help Show help for subcommands"} +{"t_ms": 196, "stream": "stderr", "data": " install Configure environment paths and shell settings"} +{"t_ms": 196, "stream": "stderr", "data": " mcp Manage MCP servers (add, remove, list, enable, disable)"} +{"t_ms": 196, "stream": "stderr", "data": " mic-serve Serve this machine's microphone to a CLI on another host"} +{"t_ms": 196, "stream": "stderr", "data": " models List available models"} +{"t_ms": 196, "stream": "stderr", "data": " plugin Manage plugins (install, uninstall, list, enable, disable)"} +{"t_ms": 196, "stream": "stderr", "data": " plugins Alias for plugin"} +{"t_ms": 196, "stream": "stderr", "data": " remote-control Manage the remote-control background daemon (start, status, stop)"} +{"t_ms": 196, "stream": "stderr", "data": " update Update CLI"} diff --git a/plugins/antigravity-provider/test/fixtures/help.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/help.stdin.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/help.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/help.stdout.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/image-content.capture.ndjson b/plugins/antigravity-provider/test/fixtures/image-content.capture.ndjson new file mode 100644 index 0000000000..a2351ec906 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/image-content.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--disable-slash-commands"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 383, "stream": "capture", "data": {"exit_code": 1}} diff --git a/plugins/antigravity-provider/test/fixtures/image-content.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/image-content.stderr.ndjson new file mode 100644 index 0000000000..3224b28fcc --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/image-content.stderr.ndjson @@ -0,0 +1 @@ +{"t_ms": 294, "stream": "stderr", "data": "error: stream input content block type \"image\" is not supported (only \"text\")"} diff --git a/plugins/antigravity-provider/test/fixtures/image-content.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/image-content.stdin.ndjson new file mode 100644 index 0000000000..fd1966ad02 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/image-content.stdin.ndjson @@ -0,0 +1 @@ +{"t_ms": 294, "stream": "stdin", "data": {"event": "user", "message": {"content": [{"type": "text", "text": "Describe this image."}, {"type": "image", "source": {"type": "base64", "media_type": "image/png", "data": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aN1sAAAAASUVORK5CYII="}}]}}} diff --git a/plugins/antigravity-provider/test/fixtures/image-content.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/image-content.stdout.ndjson new file mode 100644 index 0000000000..07cb4f0703 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/image-content.stdout.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 294, "stream": "stdout", "data": {"event": "init", "conversation_id": "71e19e79-63e3-4a7b-b628-822e69618edc", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "request-review"}}} +{"t_ms": 294, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "71e19e79-63e3-4a7b-b628-822e69618edc", "status": "ERROR", "response": "", "error": "stream input content block type \"image\" is not supported (only \"text\")", "duration_seconds": 0, "num_turns": 0, "usage": {"input_tokens": 0, "output_tokens": 0, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 0}}}} diff --git a/plugins/antigravity-provider/test/fixtures/image-file-external.capture.ndjson b/plugins/antigravity-provider/test/fixtures/image-file-external.capture.ndjson new file mode 100644 index 0000000000..90d75443bc --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/image-file-external.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", "", "--disable-slash-commands"], "cwd": "", "home": "/image-file-external-home", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 7877, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/image-file-external.png b/plugins/antigravity-provider/test/fixtures/image-file-external.png new file mode 100644 index 0000000000000000000000000000000000000000..de622fdd72dd12f017ac61ed74038f2e449c7422 GIT binary patch literal 910 zcmeAS@N?(olHy`uVBq!ia0vp^TR@nD2}o{QKQWbofjQLE#WAE}&fB{Ob3+_OTmso2 zDx9#_xb#CIGO1Bx!~c@t>{Uzns*Pi$^rMdqwSfOd*oe2kzpL`IortR@I9=kr) z_9R&u{!sRzmB1sZJaN$!O@(+?iM_8{PmOrNjOj~n7L zU9yKZhO)`e1}YS`;5+dk*&VF#cA?DVgU3Gsov7b;*yu-uv`km-k-&)uj~@aGiT5qy zu?qv*7F}>9^}|LL3z25$jbANZsr`r$mFdzw7I^&tSe|S`~`?KWu0*!A1eZMZ%-_uiWx@#_Uww$FLFSTA`pzr}-&ssA_CH@;H)_Os#f z!dFt;et{^F2NF?t59Y}i+*#kp(6{g^tIP++<7Z!eK5V%C*PjBOwfp``0_9fP*~k>H z=I1|t=hb15%!h(AYe^G&@#^;bX@v(1cLPlY>a~|yyqf!l*5jtf6JK#Wkcho|&`wfZ zhCggM!|@ldq|8q4cn}eE_XrW`a}6ZO@AWbS$uAX%xe3?$vxD+0-Q z|Fe&jEzG`MG19Z(`_iLh*Yr5x{86BgpZWbG>q$z&Br1NxF8WsJOxBLHOTc`_;OXk; Jvd$@?2>_QFt-JsL literal 0 HcmV?d00001 diff --git a/plugins/antigravity-provider/test/fixtures/image-file-external.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/image-file-external.stderr.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/image-file-external.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/image-file-external.stdin.ndjson new file mode 100644 index 0000000000..8dcd1a238a --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/image-file-external.stdin.ndjson @@ -0,0 +1 @@ +{"t_ms": 320, "stream": "stdin", "data": {"event": "user", "message": {"content": "Use view_file to open the attached image at /owned/attachment.png. Describe the shapes and colors visible. Do not use shell or code to infer the pixels."}}} diff --git a/plugins/antigravity-provider/test/fixtures/image-file-external.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/image-file-external.stdout.ndjson new file mode 100644 index 0000000000..77f1580b85 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/image-file-external.stdout.ndjson @@ -0,0 +1,14 @@ +{"t_ms": 320, "stream": "stdout", "data": {"event": "init", "conversation_id": "0c498203-4202-446f-8df1-dfa8f16cb19d", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "request-review"}}} +{"t_ms": 347, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0c498203-4202-446f-8df1-dfa8f16cb19d", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 4057, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0c498203-4202-446f-8df1-dfa8f16cb19d", "step_index": 1, "state": "DONE", "step_type": "agent_response", "duration_seconds": 3.704260379, "usage": {"input_tokens": 11275, "output_tokens": 384, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11659}}}} +{"t_ms": 4057, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0c498203-4202-446f-8df1-dfa8f16cb19d", "step_index": 2, "state": "ACTIVE", "step_type": "tool", "tool_name": "view_file", "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/owned/attachment.png"}}}}} +{"t_ms": 4057, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0c498203-4202-446f-8df1-dfa8f16cb19d", "step_index": 2, "state": "DONE", "step_type": "tool", "tool_name": "view_file", "duration_seconds": 0.003012462, "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/owned/attachment.png"}}}}} +{"t_ms": 7530, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0c498203-4202-446f-8df1-dfa8f16cb19d", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "Based on the image at [/"}}} +{"t_ms": 7555, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0c498203-4202-446f-8df1-dfa8f16cb19d", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "tmp/agy-p2/o"}}} +{"t_ms": 7581, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0c498203-4202-446f-8df1-dfa8f16cb19d", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "wned/attachment.png](fil"}}} +{"t_ms": 7606, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0c498203-4202-446f-8df1-dfa8f16cb19d", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "e:///tmp/agy"}}} +{"t_ms": 7631, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0c498203-4202-446f-8df1-dfa8f16cb19d", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "-p2/owned/attachment.png"}}} +{"t_ms": 7657, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0c498203-4202-446f-8df1-dfa8f16cb19d", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "), here are "}}} +{"t_ms": 7682, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0c498203-4202-446f-8df1-dfa8f16cb19d", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "the visible shapes and colors:\n\n- **Red Triangl"}}} +{"t_ms": 7708, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0c498203-4202-446f-8df1-dfa8f16cb19d", "step_index": 3, "state": "DONE", "step_type": "agent_response", "text_delta": "e**: A solid red triangle positioned on the left side, pointing upwards.\n- **Blue Circle**: A solid blue circle positioned on the lower right side.\n- **Background**: Solid white.\n", "duration_seconds": 3.629267516, "usage": {"input_tokens": 13749, "output_tokens": 170, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 13919}}}} +{"t_ms": 7708, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "0c498203-4202-446f-8df1-dfa8f16cb19d", "status": "SUCCESS", "response": "Based on the image at [/owned/attachment.png](file:///owned/attachment.png), here are the visible shapes and colors:\n\n- **Red Triangle**: A solid red triangle positioned on the left side, pointing upwards.\n- **Blue Circle**: A solid blue circle positioned on the lower right side.\n- **Background**: Solid white.\n", "duration_seconds": 7.367115828, "num_turns": 1, "usage": {"input_tokens": 25024, "output_tokens": 554, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 25578}}}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-add-http.capture.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-add-http.capture.ndjson new file mode 100644 index 0000000000..c04b13268d --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-add-http.capture.ndjson @@ -0,0 +1,3 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "mcp", "add", "phase0", "http://127.0.0.1:/mcp"], "cwd": "", "home": "", "settings": null, "env": {}}} +{"t_ms": 68, "stream": "capture", "data": {"written_file": "/.gemini/config/mcp_config.json", "content": {"mcpServers": {"phase0": {"disabled": false, "serverUrl": "http://127.0.0.1:/mcp"}}}}} +{"t_ms": 69, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-add-http.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-add-http.stderr.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/mcp-add-http.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-add-http.stdin.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/mcp-add-http.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-add-http.stdout.ndjson new file mode 100644 index 0000000000..05d385caa8 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-add-http.stdout.ndjson @@ -0,0 +1 @@ +{"t_ms": 63, "stream": "stdout", "data": "Added MCP server \"phase0\" (http)"} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-add.capture.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-add.capture.ndjson new file mode 100644 index 0000000000..d115585bec --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-add.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "mcp", "add", "phase0", "--url", "http://127.0.0.1:/mcp"], "cwd": "", "home": "", "settings": null, "env": {}}} +{"t_ms": 82, "stream": "capture", "data": {"exit_code": 1}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-add.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-add.stderr.ndjson new file mode 100644 index 0000000000..81181df217 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-add.stderr.ndjson @@ -0,0 +1 @@ +{"t_ms": 76, "stream": "stderr", "data": "Error: invalid command \"--url\": flags must come before the server name (usage: mcp add [flags] [args...])"} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-add.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-add.stdin.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/mcp-add.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-add.stdout.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/mcp-agent-definition.capture.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-agent-definition.capture.ndjson new file mode 100644 index 0000000000..0885d233a5 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-agent-definition.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", "/owned", "--add-dir", "", "--agent", "phase2", "--dangerously-skip-permissions"], "cwd": "", "home": "/mcp-agent-definition-home", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 6091, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-agent-definition.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-agent-definition.stderr.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/mcp-agent-definition.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-agent-definition.stdin.ndjson new file mode 100644 index 0000000000..8daf16fd52 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-agent-definition.stdin.ndjson @@ -0,0 +1 @@ +{"t_ms": 311, "stream": "stdin", "data": {"event": "user", "message": {"content": "Call the phase2 MCP echo tool with text PHASE2_DISCOVERY. Then use run_command pwd and report the workspace directory. Do not choose a different directory."}}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-agent-definition.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-agent-definition.stdout.ndjson new file mode 100644 index 0000000000..854e8701ce --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-agent-definition.stdout.ndjson @@ -0,0 +1,15 @@ +{"t_ms": 311, "stream": "stdout", "data": {"event": "init", "conversation_id": "7240f716-f2bd-4641-b210-a4a282a756f7", "init": {"cwd": "", "agent": "phase2", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "always-proceed"}}} +{"t_ms": 340, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7240f716-f2bd-4641-b210-a4a282a756f7", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 4006, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7240f716-f2bd-4641-b210-a4a282a756f7", "step_index": 1, "state": "DONE", "step_type": "agent_response", "duration_seconds": 3.658926744, "usage": {"input_tokens": 11286, "output_tokens": 361, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11647}}}} +{"t_ms": 4006, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7240f716-f2bd-4641-b210-a4a282a756f7", "step_index": 2, "state": "ACTIVE", "step_type": "tool", "tool_name": "run_command", "tool_info": {"name": "run_command", "parameters": {"CommandLine": "pwd"}}}}} +{"t_ms": 4006, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7240f716-f2bd-4641-b210-a4a282a756f7", "step_index": 2, "state": "DONE", "step_type": "tool", "tool_name": "run_command", "duration_seconds": 0.008819631, "tool_info": {"name": "run_command", "parameters": {"CommandLine": "pwd"}, "output": "/owned\r\n"}}}} +{"t_ms": 5473, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7240f716-f2bd-4641-b210-a4a282a756f7", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "The "}}} +{"t_ms": 5603, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7240f716-f2bd-4641-b210-a4a282a756f7", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "`phase2 MCP echo tool` is not availa"}}} +{"t_ms": 5626, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7240f716-f2bd-4641-b210-a4a282a756f7", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ble in the tool definiti"}}} +{"t_ms": 5652, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7240f716-f2bd-4641-b210-a4a282a756f7", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ons provided"}}} +{"t_ms": 5678, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7240f716-f2bd-4641-b210-a4a282a756f7", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " in this"}}} +{"t_ms": 5856, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7240f716-f2bd-4641-b210-a4a282a756f7", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " environment.\n\nRunning `"}}} +{"t_ms": 5881, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7240f716-f2bd-4641-b210-a4a282a756f7", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "pwd` in the workspace re"}}} +{"t_ms": 5907, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7240f716-f2bd-4641-b210-a4a282a756f7", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ports the directory:\n`/t"}}} +{"t_ms": 5932, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7240f716-f2bd-4641-b210-a4a282a756f7", "step_index": 3, "state": "DONE", "step_type": "agent_response", "text_delta": "mp/agy-p2/owned`\n", "duration_seconds": 1.928095705, "usage": {"input_tokens": 11417, "output_tokens": 43, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11460}}}} +{"t_ms": 5932, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "7240f716-f2bd-4641-b210-a4a282a756f7", "status": "SUCCESS", "response": "The `phase2 MCP echo tool` is not available in the tool definitions provided in this environment.\n\nRunning `pwd` in the workspace reports the directory:\n`/owned`\n", "duration_seconds": 5.61440038, "num_turns": 1, "usage": {"input_tokens": 22703, "output_tokens": 404, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 23107}}}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-agent-discovered.capture.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-agent-discovered.capture.ndjson new file mode 100644 index 0000000000..c5d6ddd6fd --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-agent-discovered.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", "/owned", "--add-dir", "", "--agent", "phase2", "--dangerously-skip-permissions"], "cwd": "", "home": "/mcp-agent-discovered-home", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 5524, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-agent-discovered.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-agent-discovered.stderr.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/mcp-agent-discovered.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-agent-discovered.stdin.ndjson new file mode 100644 index 0000000000..d8cfdf92de --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-agent-discovered.stdin.ndjson @@ -0,0 +1 @@ +{"t_ms": 324, "stream": "stdin", "data": {"event": "user", "message": {"content": "Call the phase2 MCP echo tool with text PHASE2_DISCOVERY. Then use run_command pwd and report the workspace directory. Do not choose a different directory."}}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-agent-discovered.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-agent-discovered.stdout.ndjson new file mode 100644 index 0000000000..a275715465 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-agent-discovered.stdout.ndjson @@ -0,0 +1,19 @@ +{"t_ms": 324, "stream": "stdout", "data": {"event": "init", "conversation_id": "b4d0a25b-bb0a-4495-84af-247f689ba6ad", "init": {"cwd": "", "agent": "phase2", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "always-proceed"}}} +{"t_ms": 356, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b4d0a25b-bb0a-4495-84af-247f689ba6ad", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 3842, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b4d0a25b-bb0a-4495-84af-247f689ba6ad", "step_index": 1, "state": "DONE", "step_type": "agent_response", "duration_seconds": 3.487178538, "usage": {"input_tokens": 11279, "output_tokens": 433, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11712}}}} +{"t_ms": 3842, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b4d0a25b-bb0a-4495-84af-247f689ba6ad", "step_index": 2, "state": "ACTIVE", "step_type": "tool", "tool_name": "run_command", "tool_info": {"name": "run_command", "parameters": {"CommandLine": "pwd"}}}}} +{"t_ms": 3867, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b4d0a25b-bb0a-4495-84af-247f689ba6ad", "step_index": 2, "state": "DONE", "step_type": "tool", "tool_name": "run_command", "duration_seconds": 0.008305961, "tool_info": {"name": "run_command", "parameters": {"CommandLine": "pwd"}, "output": "/owned\r\n"}}}} +{"t_ms": 5050, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b4d0a25b-bb0a-4495-84af-247f689ba6ad", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "1."}}} +{"t_ms": 5177, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b4d0a25b-bb0a-4495-84af-247f689ba6ad", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " **MCP Echo "}}} +{"t_ms": 5203, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b4d0a25b-bb0a-4495-84af-247f689ba6ad", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "Tool**: There is no MCP "}}} +{"t_ms": 5228, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b4d0a25b-bb0a-4495-84af-247f689ba6ad", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "`echo` tool "}}} +{"t_ms": 5254, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b4d0a25b-bb0a-4495-84af-247f689ba6ad", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "registered or available "}}} +{"t_ms": 5279, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b4d0a25b-bb0a-4495-84af-247f689ba6ad", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "in the current environme"}}} +{"t_ms": 5304, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b4d0a25b-bb0a-4495-84af-247f689ba6ad", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "nt/tool definit"}}} +{"t_ms": 5330, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b4d0a25b-bb0a-4495-84af-247f689ba6ad", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ions.\n2. **Workspace Dir"}}} +{"t_ms": 5355, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b4d0a25b-bb0a-4495-84af-247f689ba6ad", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ectory**: Ra"}}} +{"t_ms": 5381, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b4d0a25b-bb0a-4495-84af-247f689ba6ad", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "n `pwd` in the current w"}}} +{"t_ms": 5406, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b4d0a25b-bb0a-4495-84af-247f689ba6ad", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "orkspace directory and o"}}} +{"t_ms": 5432, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b4d0a25b-bb0a-4495-84af-247f689ba6ad", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "btained:\n ```\n /tmp/"}}} +{"t_ms": 5457, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b4d0a25b-bb0a-4495-84af-247f689ba6ad", "step_index": 3, "state": "DONE", "step_type": "agent_response", "text_delta": "agy-p2/owned\n ```\n", "duration_seconds": 1.60141087, "usage": {"input_tokens": 11407, "output_tokens": 62, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11469}}}} +{"t_ms": 5457, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "b4d0a25b-bb0a-4495-84af-247f689ba6ad", "status": "SUCCESS", "response": "1. **MCP Echo Tool**: There is no MCP `echo` tool registered or available in the current environment/tool definitions.\n2. **Workspace Directory**: Ran `pwd` in the current workspace directory and obtained:\n ```\n /owned\n ```\n", "duration_seconds": 5.118791831, "num_turns": 1, "usage": {"input_tokens": 22686, "output_tokens": 495, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 23181}}}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-agent-file.capture.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-agent-file.capture.ndjson new file mode 100644 index 0000000000..57d8da6b18 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-agent-file.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--disable-slash-commands", "--dangerously-skip-permissions", "--add-dir", "", "--agent", "/owned/agent.md"], "cwd": "", "home": "/mcp-agent-file-home", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 12172, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-agent-file.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-agent-file.stderr.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/mcp-agent-file.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-agent-file.stdin.ndjson new file mode 100644 index 0000000000..21feb61a76 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-agent-file.stdin.ndjson @@ -0,0 +1 @@ +{"t_ms": 305, "stream": "stdin", "data": {"event": "user", "message": {"content": "First call the phase2 MCP echo tool with text PHASE2_DISCOVERY. If unavailable say MCP_UNAVAILABLE. Then use shell pwd, read input.txt and write output.txt containing PHASE2_WRITE. Use the workspace default directory, without me supplying absolute paths. Report the exact paths used."}}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-agent-file.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-agent-file.stdout.ndjson new file mode 100644 index 0000000000..3d172dbe90 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-agent-file.stdout.ndjson @@ -0,0 +1,39 @@ +{"t_ms": 305, "stream": "stdout", "data": {"event": "init", "conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "init": {"cwd": "", "agent": "/owned/agent.md", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "always-proceed"}}} +{"t_ms": 334, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 4780, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 1, "state": "DONE", "step_type": "agent_response", "duration_seconds": 4.446916168, "usage": {"input_tokens": 11298, "output_tokens": 523, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11821}}}} +{"t_ms": 4780, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 2, "state": "ACTIVE", "step_type": "tool", "tool_name": "run_command", "tool_info": {"name": "run_command", "parameters": {"CommandLine": "pwd"}}}}} +{"t_ms": 4780, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 2, "state": "DONE", "step_type": "tool", "tool_name": "run_command", "duration_seconds": 0.007696421, "tool_info": {"name": "run_command", "parameters": {"CommandLine": "pwd"}, "output": "\r\n"}}}} +{"t_ms": 6843, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 3, "state": "DONE", "step_type": "agent_response", "duration_seconds": 2.062108191, "usage": {"input_tokens": 11436, "output_tokens": 47, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11483}}}} +{"t_ms": 6843, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 4, "state": "ACTIVE", "step_type": "tool", "tool_name": "view_file", "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/input.txt"}}}}} +{"t_ms": 6869, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 4, "state": "DONE", "step_type": "tool", "tool_name": "view_file", "duration_seconds": 0.002313647, "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/input.txt"}, "output": "1 lines, 23 bytes"}}}} +{"t_ms": 8781, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 5, "state": "DONE", "step_type": "agent_response", "duration_seconds": 1.919162211, "usage": {"input_tokens": 11654, "output_tokens": 79, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11733}}}} +{"t_ms": 8781, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 6, "state": "ACTIVE", "step_type": "tool", "tool_name": "write_to_file", "tool_info": {"name": "write_to_file", "parameters": {"TargetFile": "/output.txt"}}}}} +{"t_ms": 8781, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 6, "state": "DONE", "step_type": "tool", "tool_name": "write_to_file", "duration_seconds": 0.003384418, "tool_info": {"name": "write_to_file", "parameters": {"TargetFile": "/output.txt"}}}}} +{"t_ms": 11046, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "MCP_"}}} +{"t_ms": 11224, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "UNAVAILABLE\n\n### Execution Summary\n\n"}}} +{"t_ms": 11249, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "1. **MCP Ech"}}} +{"t_ms": 11275, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "o Tool Check**:\n"}}} +{"t_ms": 11402, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " - Status: MCP echo to"}}} +{"t_ms": 11427, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ol was not a"}}} +{"t_ms": 11452, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "vailable in the active e"}}} +{"t_ms": 11478, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "nvironment tools (`MCP_U"}}} +{"t_ms": 11504, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "NAVAILABLE`).\n\n2. **Work"}}} +{"t_ms": 11529, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ing Directory (`pwd`)**:"}}} +{"t_ms": 11554, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "\n - Output"}}} +{"t_ms": 11580, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": ": `"}}} +{"t_ms": 11605, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "`\n\n3. **Read File**:"}}} +{"t_ms": 11630, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "\n - Path: "}}} +{"t_ms": 11656, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "[input.txt](file:///tmp/"}}} +{"t_ms": 11681, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "agy-p2/works"}}} +{"t_ms": 11706, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "pace/"}}} +{"t_ms": 11732, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "input.txt)\n "}}} +{"t_ms": 11757, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " - Content: `PHASE2_WOR"}}} +{"t_ms": 11783, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "KSPACE_MARKER`\n\n4. **Wri"}}} +{"t_ms": 11808, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "tten"}}} +{"t_ms": 11859, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " File**:\n "}}} +{"t_ms": 11885, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "- Path: [output.txt](fil"}}} +{"t_ms": 11910, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "e:///workspac"}}} +{"t_ms": 11936, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "e/output.txt)\n - C"}}} +{"t_ms": 11961, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ontent: `PHASE2_WRITE`"}}} +{"t_ms": 12037, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "step_index": 7, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 3.264948634, "usage": {"input_tokens": 11821, "output_tokens": 155, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11976}}}} +{"t_ms": 12063, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "a6983f10-58bf-4679-bd90-b66865c3bf93", "status": "SUCCESS", "response": "MCP_UNAVAILABLE\n\n### Execution Summary\n\n1. **MCP Echo Tool Check**:\n - Status: MCP echo tool was not available in the active environment tools (`MCP_UNAVAILABLE`).\n\n2. **Working Directory (`pwd`)**:\n - Output: ``\n\n3. **Read File**:\n - Path: [input.txt](file:///input.txt)\n - Content: `PHASE2_WORKSPACE_MARKER`\n\n4. **Written File**:\n - Path: [output.txt](file:///output.txt)\n - Content: `PHASE2_WRITE`\n", "duration_seconds": 11.727995605, "num_turns": 1, "usage": {"input_tokens": 46209, "output_tokens": 804, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 47013}}}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-global.capture.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-global.capture.ndjson new file mode 100644 index 0000000000..452d5e7036 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-global.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", "", "--disable-slash-commands", "--dangerously-skip-permissions"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 9237, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-global.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-global.stderr.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/mcp-global.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-global.stdin.ndjson new file mode 100644 index 0000000000..cf779b2f3b --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-global.stdin.ndjson @@ -0,0 +1 @@ +{"t_ms": 300, "stream": "stdin", "data": {"event": "user", "message": {"content": "Call the phase0 MCP echo tool with text PHASE0_MCP. Do not use shell. If unavailable say UNAVAILABLE."}}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-global.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-global.stdout.ndjson new file mode 100644 index 0000000000..06b16f0005 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-global.stdout.ndjson @@ -0,0 +1,14 @@ +{"t_ms": 300, "stream": "stdout", "data": {"event": "init", "conversation_id": "e21f27e1-de55-4f15-a93f-9f70d726915e", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "always-proceed"}}} +{"t_ms": 328, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e21f27e1-de55-4f15-a93f-9f70d726915e", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 3784, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e21f27e1-de55-4f15-a93f-9f70d726915e", "step_index": 1, "state": "DONE", "step_type": "agent_response", "duration_seconds": 3.433302844, "usage": {"input_tokens": 11924, "output_tokens": 406, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12330}}}} +{"t_ms": 3784, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e21f27e1-de55-4f15-a93f-9f70d726915e", "step_index": 2, "state": "ACTIVE", "step_type": "tool", "tool_name": "view_file", "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/.gemini/antigravity-cli/mcp/phase0/echo.json"}}}}} +{"t_ms": 3784, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e21f27e1-de55-4f15-a93f-9f70d726915e", "step_index": 2, "state": "DONE", "step_type": "tool", "tool_name": "view_file", "duration_seconds": 0.003012222, "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/.gemini/antigravity-cli/mcp/phase0/echo.json"}, "output": "1 lines, 184 bytes"}}}} +{"t_ms": 5749, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e21f27e1-de55-4f15-a93f-9f70d726915e", "step_index": 3, "state": "DONE", "step_type": "agent_response", "duration_seconds": 1.966909166, "usage": {"input_tokens": 12221, "output_tokens": 63, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12284}}}} +{"t_ms": 5749, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e21f27e1-de55-4f15-a93f-9f70d726915e", "step_index": 4, "state": "ACTIVE", "step_type": "tool", "tool_name": "call_mcp_tool", "tool_info": {"name": "call_mcp_tool", "parameters": {"Arguments": "{\"text\": \"PHASE0_MCP\"}", "ServerName": "phase0", "ToolName": "echo"}}}}} +{"t_ms": 5749, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e21f27e1-de55-4f15-a93f-9f70d726915e", "step_index": 4, "state": "ERROR", "step_type": "tool", "tool_name": "call_mcp_tool", "duration_seconds": 0.004579062, "tool_info": {"name": "call_mcp_tool", "parameters": {"Arguments": "{\"text\": \"PHASE0_MCP\"}", "ServerName": "phase0", "ToolName": "echo"}, "error": {"type": "TOOL_ERROR", "message": "calling \"tools/call\": [\n {\n \"expected\": \"record\",\n \"code\": \"invalid_type\",\n \"path\": [\n \"params\",\n \"arguments\"\n ],\n \"message\": \"Invalid input: expected record, received string\"\n }\n]"}}}}} +{"t_ms": 7536, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e21f27e1-de55-4f15-a93f-9f70d726915e", "step_index": 5, "state": "DONE", "step_type": "agent_response", "duration_seconds": 1.781658824, "usage": {"input_tokens": 12407, "output_tokens": 60, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12467}}}} +{"t_ms": 7536, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e21f27e1-de55-4f15-a93f-9f70d726915e", "step_index": 6, "state": "ACTIVE", "step_type": "tool", "tool_name": "call_mcp_tool", "tool_info": {"name": "call_mcp_tool", "parameters": {"Arguments": "{\"text\":\"PHASE0_MCP\"}", "ServerName": "phase0", "ToolName": "echo"}}}}} +{"t_ms": 7536, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e21f27e1-de55-4f15-a93f-9f70d726915e", "step_index": 6, "state": "ERROR", "step_type": "tool", "tool_name": "call_mcp_tool", "duration_seconds": 0.003570676, "tool_info": {"name": "call_mcp_tool", "parameters": {"Arguments": "{\"text\":\"PHASE0_MCP\"}", "ServerName": "phase0", "ToolName": "echo"}, "error": {"type": "TOOL_ERROR", "message": "calling \"tools/call\": [\n {\n \"expected\": \"record\",\n \"code\": \"invalid_type\",\n \"path\": [\n \"params\",\n \"arguments\"\n ],\n \"message\": \"Invalid input: expected record, received string\"\n }\n]"}}}}} +{"t_ms": 8744, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e21f27e1-de55-4f15-a93f-9f70d726915e", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "UNAVAILABLE"}}} +{"t_ms": 9073, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e21f27e1-de55-4f15-a93f-9f70d726915e", "step_index": 7, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 1.522542972, "usage": {"input_tokens": 12590, "output_tokens": 2, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12592}}}} +{"t_ms": 9073, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "e21f27e1-de55-4f15-a93f-9f70d726915e", "status": "SUCCESS", "response": "UNAVAILABLE\n", "duration_seconds": 8.74880144, "num_turns": 1, "usage": {"input_tokens": 49142, "output_tokens": 531, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 49673}}}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-help.capture.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-help.capture.ndjson new file mode 100644 index 0000000000..64ea60355d --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-help.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "mcp", "add", "--help"], "cwd": "", "home": "", "settings": null, "env": {}}} +{"t_ms": 75, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-help.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-help.stderr.ndjson new file mode 100644 index 0000000000..93f2f5abb9 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-help.stderr.ndjson @@ -0,0 +1,28 @@ +{"t_ms": 69, "stream": "stderr", "data": "Usage: agy mcp add [flags] [args...]"} +{"t_ms": 69, "stream": "stderr", "data": ""} +{"t_ms": 69, "stream": "stderr", "data": "Add or update an MCP server configuration"} +{"t_ms": 69, "stream": "stderr", "data": ""} +{"t_ms": 69, "stream": "stderr", "data": "Flags:"} +{"t_ms": 69, "stream": "stderr", "data": " -H Short alias for --header (default [])"} +{"t_ms": 69, "stream": "stderr", "data": " -e Short alias for --env (default [])"} +{"t_ms": 69, "stream": "stderr", "data": " --env Environment variable KEY=value (repeatable) (default [])"} +{"t_ms": 69, "stream": "stderr", "data": " -h Show help"} +{"t_ms": 69, "stream": "stderr", "data": " --header HTTP header Key: Value (repeatable) (default [])"} +{"t_ms": 69, "stream": "stderr", "data": " --help Show help"} +{"t_ms": 69, "stream": "stderr", "data": " -t Short alias for --type (default stdio)"} +{"t_ms": 69, "stream": "stderr", "data": " --type Server type: 'stdio' or 'http' (default stdio)"} +{"t_ms": 69, "stream": "stderr", "data": ""} +{"t_ms": 69, "stream": "stderr", "data": "Arguments:"} +{"t_ms": 69, "stream": "stderr", "data": " Unique name for the server."} +{"t_ms": 69, "stream": "stderr", "data": " Executable for a stdio server, or the URL for an http server."} +{"t_ms": 69, "stream": "stderr", "data": " [args...] Arguments passed to the stdio command."} +{"t_ms": 69, "stream": "stderr", "data": ""} +{"t_ms": 69, "stream": "stderr", "data": "Notes:"} +{"t_ms": 69, "stream": "stderr", "data": " Flags must come before ; a flag placed after it is rejected."} +{"t_ms": 69, "stream": "stderr", "data": " http/https URLs are detected automatically, so --type is usually unnecessary."} +{"t_ms": 69, "stream": "stderr", "data": " Use -- before the command to pass a command or args that begin with '-'."} +{"t_ms": 69, "stream": "stderr", "data": ""} +{"t_ms": 69, "stream": "stderr", "data": "Examples:"} +{"t_ms": 69, "stream": "stderr", "data": " agy mcp add fs npx -y /work"} +{"t_ms": 69, "stream": "stderr", "data": " agy mcp add --header \": \" api https://example.com/mcp"} +{"t_ms": 69, "stream": "stderr", "data": " agy mcp add --env = gh -- docker run -i "} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-help.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-help.stdin.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/mcp-help.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-help.stdout.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/mcp-http.requests.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-http.requests.ndjson new file mode 100644 index 0000000000..ebe28a2ddb --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-http.requests.ndjson @@ -0,0 +1,20 @@ +{"stream": "http_request", "data": {"direction": "server_to_mcp", "message": {"jsonrpc": "2.0", "id": 1, "method": "server/discover", "params": {"_meta": {"io.modelcontextprotocol/clientCapabilities": {"elicitation": {"form": {}, "url": {}}, "roots": {"listChanged": true}}, "io.modelcontextprotocol/clientInfo": {"name": "antigravity-client", "version": "v1.0.0"}, "io.modelcontextprotocol/protocolVersion": "2026-07-28"}}}}} +{"stream": "http_request", "data": {"direction": "server_to_mcp", "message": {"jsonrpc": "2.0", "id": 2, "method": "initialize", "params": {"clientInfo": {"name": "antigravity-client", "version": "v1.0.0"}, "protocolVersion": "2025-11-25", "capabilities": {"elicitation": {"form": {}, "url": {}}, "roots": {"listChanged": true}}}}}} +{"stream": "http_request", "data": {"direction": "server_to_mcp", "message": {"jsonrpc": "2.0", "method": "notifications/roots/list_changed", "params": {}}}} +{"stream": "http_request", "data": {"direction": "server_to_mcp", "message": {"jsonrpc": "2.0", "method": "notifications/initialized", "params": {}}}} +{"stream": "http_request", "data": {"direction": "server_to_mcp", "message": {"jsonrpc": "2.0", "id": 3, "method": "tools/list", "params": {}}}} +{"stream": "http_request", "data": {"direction": "server_to_mcp", "message": {"jsonrpc": "2.0", "method": "notifications/roots/list_changed", "params": {}}}} +{"stream": "http_request", "data": {"direction": "server_to_mcp", "message": {"jsonrpc": "2.0", "id": 4, "method": "tools/call", "params": {"_meta": {"antigravity.google/artifacts_dir": "/.gemini/antigravity-cli/brain/e21f27e1-de55-4f15-a93f-9f70d726915e", "antigravity.google/conversation_id": "e21f27e1-de55-4f15-a93f-9f70d726915e", "progressToken": "88c11f68-c1b5-46f7-ae6d-1cb662c4ca0c:4"}, "name": "echo", "arguments": "{\"text\": \"PHASE0_MCP\"}"}}}} +{"stream": "http_request", "data": {"direction": "server_to_mcp", "message": {"jsonrpc": "2.0", "id": 5, "method": "tools/call", "params": {"_meta": {"antigravity.google/artifacts_dir": "/.gemini/antigravity-cli/brain/e21f27e1-de55-4f15-a93f-9f70d726915e", "antigravity.google/conversation_id": "e21f27e1-de55-4f15-a93f-9f70d726915e", "progressToken": "88c11f68-c1b5-46f7-ae6d-1cb662c4ca0c:6"}, "name": "echo", "arguments": "{\"text\":\"PHASE0_MCP\"}"}}}} +{"stream": "http_request", "data": {"direction": "server_to_mcp", "message": {"jsonrpc": "2.0", "id": 1, "method": "server/discover", "params": {"_meta": {"io.modelcontextprotocol/clientCapabilities": {"elicitation": {"form": {}, "url": {}}, "roots": {"listChanged": true}}, "io.modelcontextprotocol/clientInfo": {"name": "antigravity-client", "version": "v1.0.0"}, "io.modelcontextprotocol/protocolVersion": "2026-07-28"}}}}} +{"stream": "http_request", "data": {"direction": "server_to_mcp", "message": {"jsonrpc": "2.0", "id": 2, "method": "initialize", "params": {"clientInfo": {"name": "antigravity-client", "version": "v1.0.0"}, "protocolVersion": "2025-11-25", "capabilities": {"elicitation": {"form": {}, "url": {}}, "roots": {"listChanged": true}}}}}} +{"stream": "http_request", "data": {"direction": "server_to_mcp", "message": {"jsonrpc": "2.0", "method": "notifications/initialized", "params": {}}}} +{"stream": "http_request", "data": {"direction": "server_to_mcp", "message": {"jsonrpc": "2.0", "id": 3, "method": "tools/list", "params": {}}}} +{"stream": "http_request", "data": {"direction": "server_to_mcp", "message": {"jsonrpc": "2.0", "id": 4, "method": "tools/call", "params": {"_meta": {"antigravity.google/artifacts_dir": "/.gemini/antigravity-cli/brain/89273b5a-77cd-4d4b-ac80-4475b2c751d0", "antigravity.google/conversation_id": "89273b5a-77cd-4d4b-ac80-4475b2c751d0", "progressToken": "c009d4b2-d4f6-44f3-9a29-1b0092eb123f:4"}, "name": "echo", "arguments": "{\"text\":\"PHASE0_MCP\"}"}}}} +{"stream": "http_request", "data": {"direction": "server_to_mcp", "message": {"jsonrpc": "2.0", "id": 5, "method": "tools/call", "params": {"_meta": {"antigravity.google/artifacts_dir": "/.gemini/antigravity-cli/brain/89273b5a-77cd-4d4b-ac80-4475b2c751d0", "antigravity.google/conversation_id": "89273b5a-77cd-4d4b-ac80-4475b2c751d0", "progressToken": "c009d4b2-d4f6-44f3-9a29-1b0092eb123f:6"}, "name": "echo", "arguments": "{\"text\": \"PHASE0_MCP\"}"}}}} +{"stream": "http_request", "data": {"direction": "server_to_mcp", "message": {"jsonrpc": "2.0", "id": 1, "method": "server/discover", "params": {"_meta": {"io.modelcontextprotocol/clientCapabilities": {"elicitation": {"form": {}, "url": {}}, "roots": {"listChanged": true}}, "io.modelcontextprotocol/clientInfo": {"name": "antigravity-client", "version": "v1.0.0"}, "io.modelcontextprotocol/protocolVersion": "2026-07-28"}}}}} +{"stream": "http_request", "data": {"direction": "server_to_mcp", "message": {"jsonrpc": "2.0", "id": 2, "method": "initialize", "params": {"clientInfo": {"name": "antigravity-client", "version": "v1.0.0"}, "protocolVersion": "2025-11-25", "capabilities": {"elicitation": {"form": {}, "url": {}}, "roots": {"listChanged": true}}}}}} +{"stream": "http_request", "data": {"direction": "server_to_mcp", "message": {"jsonrpc": "2.0", "method": "notifications/initialized", "params": {}}}} +{"stream": "http_request", "data": {"direction": "server_to_mcp", "message": {"jsonrpc": "2.0", "id": 3, "method": "tools/list", "params": {}}}} +{"stream": "http_request", "data": {"direction": "server_to_mcp", "message": {"jsonrpc": "2.0", "id": 4, "method": "tools/call", "params": {"_meta": {"antigravity.google/artifacts_dir": "/.gemini/antigravity-cli/brain/0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "antigravity.google/conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "progressToken": "d3dee97c-770c-4268-86af-baa39125d7c0:4"}, "name": "echo", "arguments": "{\"text\":\"PHASE0_MCP\"}"}}}} +{"stream": "http_request", "data": {"direction": "server_to_mcp", "message": {"jsonrpc": "2.0", "id": 5, "method": "tools/call", "params": {"_meta": {"antigravity.google/artifacts_dir": "/.gemini/antigravity-cli/brain/0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "antigravity.google/conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "progressToken": "d3dee97c-770c-4268-86af-baa39125d7c0:6"}, "name": "echo", "arguments": "null"}}}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-list.capture.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-list.capture.ndjson new file mode 100644 index 0000000000..a7a6d66835 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-list.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "mcp", "list"], "cwd": "", "home": "", "settings": null, "env": {}}} +{"t_ms": 71, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-list.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-list.stderr.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/mcp-list.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-list.stdin.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/mcp-list.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-list.stdout.ndjson new file mode 100644 index 0000000000..644935d9f8 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-list.stdout.ndjson @@ -0,0 +1 @@ +{"t_ms": 65, "stream": "stdout", "data": "No MCP servers configured."} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-owned-cwd.capture.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-owned-cwd.capture.ndjson new file mode 100644 index 0000000000..ec822f7c19 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-owned-cwd.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--disable-slash-commands", "--dangerously-skip-permissions", "--add-dir", ""], "cwd": "/owned-parent/workspace", "home": "/mcp-owned-cwd-home", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}, "cwd_realpath": "/owned"}} +{"t_ms": 21316, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-owned-cwd.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-owned-cwd.stderr.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/mcp-owned-cwd.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-owned-cwd.stdin.ndjson new file mode 100644 index 0000000000..4606a29d9d --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-owned-cwd.stdin.ndjson @@ -0,0 +1 @@ +{"t_ms": 299, "stream": "stdin", "data": {"event": "user", "message": {"content": "First call the phase2 MCP echo tool with text PHASE2_DISCOVERY. If unavailable say MCP_UNAVAILABLE. Then use shell pwd, read input.txt and write output.txt containing PHASE2_WRITE. Use the workspace default directory, without me supplying absolute paths. Report the exact paths used."}}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-owned-cwd.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-owned-cwd.stdout.ndjson new file mode 100644 index 0000000000..fd49e1756a --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-owned-cwd.stdout.ndjson @@ -0,0 +1,95 @@ +{"t_ms": 299, "stream": "stdout", "data": {"event": "init", "conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "init": {"cwd": "/owned", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "always-proceed"}}} +{"t_ms": 353, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 4289, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 1, "state": "DONE", "step_type": "agent_response", "duration_seconds": 3.923817807, "usage": {"input_tokens": 11986, "output_tokens": 312, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12298}}}} +{"t_ms": 4289, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 2, "state": "ACTIVE", "step_type": "tool", "tool_name": "view_file", "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/mcp-owned-cwd-home/.gemini/antigravity-cli/mcp/phase2/echo.json"}}}}} +{"t_ms": 4289, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 2, "state": "DONE", "step_type": "tool", "tool_name": "view_file", "duration_seconds": 0.003110546, "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/mcp-owned-cwd-home/.gemini/antigravity-cli/mcp/phase2/echo.json"}, "output": "1 lines, 184 bytes"}}}} +{"t_ms": 6028, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 3, "state": "DONE", "step_type": "agent_response", "duration_seconds": 1.727561556, "usage": {"input_tokens": 12292, "output_tokens": 64, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12356}}}} +{"t_ms": 6028, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 4, "state": "ACTIVE", "step_type": "tool", "tool_name": "call_mcp_tool", "tool_info": {"name": "call_mcp_tool", "parameters": {"Arguments": "{\"text\": \"PHASE2_DISCOVERY\"}", "ServerName": "phase2", "ToolName": "echo"}}}}} +{"t_ms": 6028, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 4, "state": "ERROR", "step_type": "tool", "tool_name": "call_mcp_tool", "duration_seconds": 0.003994388, "tool_info": {"name": "call_mcp_tool", "parameters": {"Arguments": "{\"text\": \"PHASE2_DISCOVERY\"}", "ServerName": "phase2", "ToolName": "echo"}, "error": {"type": "TOOL_ERROR", "message": "calling \"tools/call\": [\n {\n \"expected\": \"record\",\n \"code\": \"invalid_type\",\n \"path\": [\n \"params\",\n \"arguments\"\n ],\n \"message\": \"Invalid input: expected record, received string\"\n }\n]"}}}}} +{"t_ms": 7764, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 5, "state": "DONE", "step_type": "agent_response", "duration_seconds": 1.73152143, "usage": {"input_tokens": 12479, "output_tokens": 63, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12542}}}} +{"t_ms": 7764, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 6, "state": "ACTIVE", "step_type": "tool", "tool_name": "call_mcp_tool", "tool_info": {"name": "call_mcp_tool", "parameters": {"Arguments": "{\"text\":\"PHASE2_DISCOVERY\"}", "ServerName": "phase2", "ToolName": "echo"}}}}} +{"t_ms": 7764, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 6, "state": "ERROR", "step_type": "tool", "tool_name": "call_mcp_tool", "duration_seconds": 0.003571568, "tool_info": {"name": "call_mcp_tool", "parameters": {"Arguments": "{\"text\":\"PHASE2_DISCOVERY\"}", "ServerName": "phase2", "ToolName": "echo"}, "error": {"type": "TOOL_ERROR", "message": "calling \"tools/call\": [\n {\n \"expected\": \"record\",\n \"code\": \"invalid_type\",\n \"path\": [\n \"params\",\n \"arguments\"\n ],\n \"message\": \"Invalid input: expected record, received string\"\n }\n]"}}}}} +{"t_ms": 9626, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 7, "state": "DONE", "step_type": "agent_response", "duration_seconds": 1.8600456250000001, "usage": {"input_tokens": 12665, "output_tokens": 70, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12735}}}} +{"t_ms": 9626, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 8, "state": "ACTIVE", "step_type": "tool", "tool_name": "run_command", "tool_info": {"name": "run_command", "parameters": {"CommandLine": "pwd"}}}}} +{"t_ms": 9626, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 8, "state": "DONE", "step_type": "tool", "tool_name": "run_command", "duration_seconds": 0.005713043, "tool_info": {"name": "run_command", "parameters": {"CommandLine": "pwd"}, "output": "/owned\r\n"}}}} +{"t_ms": 11152, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 9, "state": "DONE", "step_type": "agent_response", "duration_seconds": 1.5154668610000002, "usage": {"input_tokens": 12800, "output_tokens": 47, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12847}}}} +{"t_ms": 11152, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 10, "state": "ACTIVE", "step_type": "tool", "tool_name": "view_file", "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/owned/input.txt"}}}}} +{"t_ms": 11152, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 10, "state": "ERROR", "step_type": "tool", "tool_name": "view_file", "duration_seconds": 0.002729213, "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/owned/input.txt"}, "error": {"type": "TOOL_ERROR", "message": "declaring permissions: cortex tool view_file: convert tool call for permissions: model output error: invalid tool call error (invalid_args) failed to read file: stat /owned/input.txt: no such file or directory"}}}}} +{"t_ms": 13856, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 11, "state": "DONE", "step_type": "agent_response", "duration_seconds": 2.6989989100000003, "usage": {"input_tokens": 12961, "output_tokens": 48, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 13009}}}} +{"t_ms": 13856, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 12, "state": "ACTIVE", "step_type": "tool", "tool_name": "view_file", "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/input.txt"}}}}} +{"t_ms": 13856, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 12, "state": "DONE", "step_type": "tool", "tool_name": "view_file", "duration_seconds": 0.002359752, "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/input.txt"}, "output": "1 lines, 23 bytes"}}}} +{"t_ms": 15999, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 13, "state": "DONE", "step_type": "agent_response", "duration_seconds": 2.144108405, "usage": {"input_tokens": 13180, "output_tokens": 78, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 13258}}}} +{"t_ms": 15999, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 14, "state": "ACTIVE", "step_type": "tool", "tool_name": "write_to_file", "tool_info": {"name": "write_to_file", "parameters": {"TargetFile": "/owned/output.txt"}}}}} +{"t_ms": 15999, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 14, "state": "DONE", "step_type": "tool", "tool_name": "write_to_file", "duration_seconds": 0.004225731, "tool_info": {"name": "write_to_file", "parameters": {"TargetFile": "/owned/output.txt"}}}}} +{"t_ms": 17563, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 15, "state": "DONE", "step_type": "agent_response", "duration_seconds": 1.5609718259999998, "usage": {"input_tokens": 13346, "output_tokens": 79, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 13425}}}} +{"t_ms": 17563, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 16, "state": "ACTIVE", "step_type": "tool", "tool_name": "write_to_file", "tool_info": {"name": "write_to_file", "parameters": {"TargetFile": "/output.txt"}}}}} +{"t_ms": 17563, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 16, "state": "DONE", "step_type": "tool", "tool_name": "write_to_file", "duration_seconds": 0.003200805, "tool_info": {"name": "write_to_file", "parameters": {"TargetFile": "/output.txt"}}}}} +{"t_ms": 18830, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "### Phase"}}} +{"t_ms": 19110, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " 2 MCP Tool Call\nThe `ph"}}} +{"t_ms": 19134, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ase2` MCP server was cal"}}} +{"t_ms": 19160, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "led with `ec"}}} +{"t_ms": 19185, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ho`, but tool execution "}}} +{"t_ms": 19211, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "returned a p"}}} +{"t_ms": 19237, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "arameter parsing error (`invalid_typ"}}} +{"t_ms": 19262, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "e: expected record, rece"}}} +{"t_ms": 19288, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ived string`"}}} +{"t_ms": 19314, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " when passing arguments)"}}} +{"t_ms": 19339, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": ". Therefore:"}}} +{"t_ms": 19365, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " **MCP_UNAVAILABLE** (or"}}} +{"t_ms": 19390, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " encounterin"}}} +{"t_ms": 19416, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "g invocation schema incompatibilitie"}}} +{"t_ms": 19441, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "s).\n\n---\n\n### Shell `pwd"}}} +{"t_ms": 19466, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "`\nExecuted `"}}} +{"t_ms": 19492, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "pwd` in the default comm"}}} +{"t_ms": 19517, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "and working "}}} +{"t_ms": 19544, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "directory:\n- **Default CWD:** `/tmp/"}}} +{"t_ms": 19570, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "agy-p2/owned`\n\n---\n\n### "}}} +{"t_ms": 19595, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "File Operati"}}} +{"t_ms": 19621, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ons\n1. **Read `input.txt"}}} +{"t_ms": 19646, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "`:**\n - Attempted: [`/o"}}} +{"t_ms": 19672, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "wned/input.t"}}} +{"t_ms": 19698, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "xt`](file:///"}}} +{"t_ms": 19723, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "owned/"}}} +{"t_ms": 19774, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "input.txt) (not present)\n - Read f"}}} +{"t_ms": 19799, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "rom:"}}} +{"t_ms": 19879, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " [`"}}} +{"t_ms": 19905, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "/input.txt`](file:///tmp"}}} +{"t_ms": 19930, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "/agy-p2/work"}}} +{"t_ms": 19956, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "space/input.txt)\n - Content: `PHAS"}}} +{"t_ms": 19981, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "E2_WORKSPACE"}}} +{"t_ms": 20006, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "_MARKER`\n\n2. **Wrote `ou"}}} +{"t_ms": 20032, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "tput.txt`:**\n"}}} +{"t_ms": 20083, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " - Written"}}} +{"t_ms": 20109, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " to: [`/tmp/"}}} +{"t_ms": 20134, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "agy-p2/owned/output.txt`"}}} +{"t_ms": 20160, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "](file:///tmp/agy-p"}}} +{"t_ms": 20185, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "2/owned/output.txt) (def"}}} +{"t_ms": 20211, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ault CWD)\n - Also writ"}}} +{"t_ms": 20237, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ten to:"}}} +{"t_ms": 20364, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " [`/tmp/agy-"}}} +{"t_ms": 20389, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "p2/workspace/output.txt`"}}} +{"t_ms": 20415, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "](file:///wor"}}} +{"t_ms": 20440, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "kspace/outpu"}}} +{"t_ms": 20465, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "t.txt) ("}}} +{"t_ms": 20491, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "active workspace directo"}}} +{"t_ms": 20517, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ry)\n - Content: `PHASE"}}} +{"t_ms": 20542, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "2_WRITE`\n\n---\n\n### Exact"}}} +{"t_ms": 20568, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " Paths Used\n"}}} +{"t_ms": 20644, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "- **Shell CWD:** `/tmp/a"}}} +{"t_ms": 20671, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "gy-p2/owned`\n- **Input p"}}} +{"t_ms": 20695, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ath:**"}}} +{"t_ms": 20747, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " [`"}}} +{"t_ms": 20773, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "/input.txt`](file:///tmp"}}} +{"t_ms": 20799, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "/agy-p2/workspace/input."}}} +{"t_ms": 20824, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "txt"}}} +{"t_ms": 20850, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": ")\n- **Output path:** [`/"}}} +{"t_ms": 20876, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "tmp/agy-p2/o"}}} +{"t_ms": 20901, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "wned/output.txt`](file:/"}}} +{"t_ms": 20927, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "//"}}} +{"t_ms": 20952, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "tmp/agy-p2/o"}}} +{"t_ms": 20978, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "wned/output.txt) &"}}} +{"t_ms": 21105, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " [`"}}} +{"t_ms": 21131, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "/output.txt`"}}} +{"t_ms": 21156, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "](file:///wor"}}} +{"t_ms": 21181, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "step_index": 17, "state": "DONE", "step_type": "agent_response", "text_delta": "kspace/output.txt)\n", "duration_seconds": 3.625222904, "usage": {"input_tokens": 13513, "output_tokens": 439, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 13952}}}} +{"t_ms": 21207, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "09af3250-b019-4549-80fe-e2fb138a02be", "status": "SUCCESS", "response": "### Phase 2 MCP Tool Call\nThe `phase2` MCP server was called with `echo`, but tool execution returned a parameter parsing error (`invalid_type: expected record, received string` when passing arguments). Therefore: **MCP_UNAVAILABLE** (or encountering invocation schema incompatibilities).\n\n---\n\n### Shell `pwd`\nExecuted `pwd` in the default command working directory:\n- **Default CWD:** `/owned`\n\n---\n\n### File Operations\n1. **Read `input.txt`:**\n - Attempted: [`/owned/input.txt`](file:///owned/input.txt) (not present)\n - Read from: [`/input.txt`](file:///input.txt)\n - Content: `PHASE2_WORKSPACE_MARKER`\n\n2. **Wrote `output.txt`:**\n - Written to: [`/owned/output.txt`](file:///owned/output.txt) (default CWD)\n - Also written to: [`/output.txt`](file:///output.txt) (active workspace directory)\n - Content: `PHASE2_WRITE`\n\n---\n\n### Exact Paths Used\n- **Shell CWD:** `/owned`\n- **Input path:** [`/input.txt`](file:///input.txt)\n- **Output path:** [`/owned/output.txt`](file:///owned/output.txt) & [`/output.txt`](file:///output.txt)\n", "duration_seconds": 20.876486639, "num_turns": 1, "usage": {"input_tokens": 115222, "output_tokens": 1200, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 116422}}}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-owned-first-add-dir.capture.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-owned-first-add-dir.capture.ndjson new file mode 100644 index 0000000000..ea2081bbd9 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-owned-first-add-dir.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--disable-slash-commands", "--dangerously-skip-permissions", "--add-dir", "/owned", "--add-dir", ""], "cwd": "", "home": "/mcp-owned-first-add-dir-home", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 23197, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-owned-first-add-dir.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-owned-first-add-dir.stderr.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/mcp-owned-first-add-dir.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-owned-first-add-dir.stdin.ndjson new file mode 100644 index 0000000000..1cb2727caa --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-owned-first-add-dir.stdin.ndjson @@ -0,0 +1 @@ +{"t_ms": 307, "stream": "stdin", "data": {"event": "user", "message": {"content": "First call the phase2 MCP echo tool with text PHASE2_DISCOVERY. If unavailable say MCP_UNAVAILABLE. Then use shell pwd, read input.txt and write output.txt containing PHASE2_WRITE. Use the workspace default directory, without me supplying absolute paths. Report the exact paths used."}}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-owned-first-add-dir.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-owned-first-add-dir.stdout.ndjson new file mode 100644 index 0000000000..8f9424660b --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-owned-first-add-dir.stdout.ndjson @@ -0,0 +1,60 @@ +{"t_ms": 307, "stream": "stdout", "data": {"event": "init", "conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "always-proceed"}}} +{"t_ms": 373, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 3416, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 1, "state": "DONE", "step_type": "agent_response", "duration_seconds": 3.021318824, "usage": {"input_tokens": 12003, "output_tokens": 294, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12297}}}} +{"t_ms": 3416, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 2, "state": "ACTIVE", "step_type": "tool", "tool_name": "view_file", "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/mcp-owned-first-add-dir-home/.gemini/antigravity-cli/mcp/phase2/echo.json"}}}}} +{"t_ms": 3416, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 2, "state": "DONE", "step_type": "tool", "tool_name": "view_file", "duration_seconds": 0.004082433, "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/mcp-owned-first-add-dir-home/.gemini/antigravity-cli/mcp/phase2/echo.json"}, "output": "1 lines, 184 bytes"}}}} +{"t_ms": 5132, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 3, "state": "DONE", "step_type": "agent_response", "duration_seconds": 1.7174826109999999, "usage": {"input_tokens": 12318, "output_tokens": 64, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12382}}}} +{"t_ms": 5133, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 4, "state": "ACTIVE", "step_type": "tool", "tool_name": "call_mcp_tool", "tool_info": {"name": "call_mcp_tool", "parameters": {"Arguments": "{\"text\":\"PHASE2_DISCOVERY\"}", "ServerName": "phase2", "ToolName": "echo"}}}}} +{"t_ms": 5133, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 4, "state": "ERROR", "step_type": "tool", "tool_name": "call_mcp_tool", "duration_seconds": 0.005855559, "tool_info": {"name": "call_mcp_tool", "parameters": {"Arguments": "{\"text\":\"PHASE2_DISCOVERY\"}", "ServerName": "phase2", "ToolName": "echo"}, "error": {"type": "TOOL_ERROR", "message": "calling \"tools/call\": [\n {\n \"expected\": \"record\",\n \"code\": \"invalid_type\",\n \"path\": [\n \"params\",\n \"arguments\"\n ],\n \"message\": \"Invalid input: expected record, received string\"\n }\n]"}}}}} +{"t_ms": 9981, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 5, "state": "DONE", "step_type": "agent_response", "duration_seconds": 4.850317858, "usage": {"input_tokens": 12505, "output_tokens": 68, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12573}}}} +{"t_ms": 9982, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 6, "state": "ACTIVE", "step_type": "tool", "tool_name": "run_command", "tool_info": {"name": "run_command", "parameters": {"CommandLine": "pwd"}}}}} +{"t_ms": 10007, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 6, "state": "DONE", "step_type": "tool", "tool_name": "run_command", "duration_seconds": 0.005511565, "tool_info": {"name": "run_command", "parameters": {"CommandLine": "pwd"}, "output": "/owned\r\n"}}}} +{"t_ms": 11982, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 7, "state": "DONE", "step_type": "agent_response", "duration_seconds": 1.9862325090000001, "usage": {"input_tokens": 12638, "output_tokens": 47, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12685}}}} +{"t_ms": 11982, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 8, "state": "ACTIVE", "step_type": "tool", "tool_name": "view_file", "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/owned/input.txt"}}}}} +{"t_ms": 11982, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 8, "state": "ERROR", "step_type": "tool", "tool_name": "view_file", "duration_seconds": 0.002185687, "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/owned/input.txt"}, "error": {"type": "TOOL_ERROR", "message": "declaring permissions: cortex tool view_file: convert tool call for permissions: model output error: invalid tool call error (invalid_args) failed to read file: stat /owned/input.txt: no such file or directory"}}}}} +{"t_ms": 13701, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 9, "state": "DONE", "step_type": "agent_response", "duration_seconds": 1.704119444, "usage": {"input_tokens": 12799, "output_tokens": 51, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12850}}}} +{"t_ms": 13701, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 10, "state": "ACTIVE", "step_type": "tool", "tool_name": "view_file", "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/input.txt"}}}}} +{"t_ms": 13701, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 10, "state": "DONE", "step_type": "tool", "tool_name": "view_file", "duration_seconds": 0.003250577, "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/input.txt"}, "output": "1 lines, 23 bytes"}}}} +{"t_ms": 15580, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 11, "state": "DONE", "step_type": "agent_response", "duration_seconds": 1.85800527, "usage": {"input_tokens": 13021, "output_tokens": 82, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 13103}}}} +{"t_ms": 15580, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 12, "state": "ACTIVE", "step_type": "tool", "tool_name": "write_to_file", "tool_info": {"name": "write_to_file", "parameters": {"TargetFile": "/owned/output.txt"}}}}} +{"t_ms": 15580, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 12, "state": "DONE", "step_type": "tool", "tool_name": "write_to_file", "duration_seconds": 0.005710137, "tool_info": {"name": "write_to_file", "parameters": {"TargetFile": "/owned/output.txt"}}}}} +{"t_ms": 17976, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 13, "state": "DONE", "step_type": "agent_response", "duration_seconds": 2.381626267, "usage": {"input_tokens": 13191, "output_tokens": 82, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 13273}}}} +{"t_ms": 17976, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 14, "state": "ACTIVE", "step_type": "tool", "tool_name": "write_to_file", "tool_info": {"name": "write_to_file", "parameters": {"TargetFile": "/output.txt"}}}}} +{"t_ms": 17976, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 14, "state": "DONE", "step_type": "tool", "tool_name": "write_to_file", "duration_seconds": 0.002994941, "tool_info": {"name": "write_to_file", "parameters": {"TargetFile": "/output.txt"}}}}} +{"t_ms": 21806, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "1"}}} +{"t_ms": 21908, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": ". **MCP Tool Call (`phas"}}} +{"t_ms": 21939, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "e2` / `echo`):**\n - At"}}} +{"t_ms": 21971, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "tempt"}}} +{"t_ms": 22074, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ed to invoke"}}} +{"t_ms": 22099, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " `phase2` `echo` with te"}}} +{"t_ms": 22127, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "xt `PHASE2_DISCOVERY`. T"}}} +{"t_ms": 22159, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "he MCP tool returned"}}} +{"t_ms": 22213, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " an interface/parameter "}}} +{"t_ms": 22249, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "error (`Invalid input: e"}}} +{"t_ms": 22268, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "xpected record, received"}}} +{"t_ms": 22293, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " string`), i"}}} +{"t_ms": 22319, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ndicating the tool could not process"}}} +{"t_ms": 22349, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " the call: `"}}} +{"t_ms": 22375, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "MCP_UNAVAILABLE`.\n\n2. **"}}} +{"t_ms": 22402, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "Shell `pwd`:**\n - Defa"}}} +{"t_ms": 22430, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ult working "}}} +{"t_ms": 22458, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "directory: `/tmp/"}}} +{"t_ms": 22509, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "agy-p2/owned`\n\n3. **File"}}} +{"t_ms": 22535, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " Operations "}}} +{"t_ms": 22569, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "& Exact Paths:**\n - **"}}} +{"t_ms": 22594, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "Input File Read:**"}}} +{"t_ms": 22619, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " [input.txt](file:///work"}}} +{"t_ms": 22645, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "space/input."}}} +{"t_ms": 22671, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "txt) (content: `PHASE2_"}}} +{"t_ms": 22726, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "WORKSPACE_MARKER`)\n - "}}} +{"t_ms": 22751, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "**Output Files Written:*"}}} +{"t_ms": 22776, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "*\n - [ou"}}} +{"t_ms": 22807, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "tput.txt](file:///owne"}}} +{"t_ms": 22833, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "d/output.txt) (content: "}}} +{"t_ms": 22859, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "`PHASE2_WRITE`)\n -"}}} +{"t_ms": 22911, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " [output.txt](file:///tm"}}} +{"t_ms": 22937, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "p/agy-p2/workspace/outpu"}}} +{"t_ms": 22964, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "t.txt) (content: `PHASE2"}}} +{"t_ms": 22990, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "_WRITE`)"}}} +{"t_ms": 23043, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "step_index": 15, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 5.067703746, "usage": {"input_tokens": 13361, "output_tokens": 225, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 13586}}}} +{"t_ms": 23043, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "acddb85b-55c5-462f-bbea-5a5bce323030", "status": "SUCCESS", "response": "1. **MCP Tool Call (`phase2` / `echo`):**\n - Attempted to invoke `phase2` `echo` with text `PHASE2_DISCOVERY`. The MCP tool returned an interface/parameter error (`Invalid input: expected record, received string`), indicating the tool could not process the call: `MCP_UNAVAILABLE`.\n\n2. **Shell `pwd`:**\n - Default working directory: `/owned`\n\n3. **File Operations & Exact Paths:**\n - **Input File Read:** [input.txt](file:///input.txt) (content: `PHASE2_WORKSPACE_MARKER`)\n - **Output Files Written:**\n - [output.txt](file:///owned/output.txt) (content: `PHASE2_WRITE`)\n - [output.txt](file:///output.txt) (content: `PHASE2_WRITE`)\n", "duration_seconds": 22.712812313, "num_turns": 1, "usage": {"input_tokens": 101836, "output_tokens": 913, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 102749}}}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-project-config.capture.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-project-config.capture.ndjson new file mode 100644 index 0000000000..97c48693ea --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-project-config.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", "", "--disable-slash-commands", "--dangerously-skip-permissions"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 3445, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-project-config.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-project-config.stderr.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/mcp-project-config.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-project-config.stdin.ndjson new file mode 100644 index 0000000000..f5fa2b6685 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-project-config.stdin.ndjson @@ -0,0 +1 @@ +{"t_ms": 297, "stream": "stdin", "data": {"event": "user", "message": {"content": "Call the phase0 MCP echo tool with text PHASE0_MCP. Do not use shell. If unavailable say UNAVAILABLE."}}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-project-config.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-project-config.stdout.ndjson new file mode 100644 index 0000000000..c77058cdf8 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-project-config.stdout.ndjson @@ -0,0 +1,5 @@ +{"t_ms": 297, "stream": "stdout", "data": {"event": "init", "conversation_id": "0e78dd7a-7278-49b7-8cd6-68b9c0dfe82b", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "always-proceed"}}} +{"t_ms": 325, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0e78dd7a-7278-49b7-8cd6-68b9c0dfe82b", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 2917, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0e78dd7a-7278-49b7-8cd6-68b9c0dfe82b", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "UNAVAILABLE"}}} +{"t_ms": 3272, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0e78dd7a-7278-49b7-8cd6-68b9c0dfe82b", "step_index": 1, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 2.949354512, "usage": {"input_tokens": 11261, "output_tokens": 222, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11483}}}} +{"t_ms": 3298, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "0e78dd7a-7278-49b7-8cd6-68b9c0dfe82b", "status": "SUCCESS", "response": "UNAVAILABLE\n", "duration_seconds": 2.972410821, "num_turns": 1, "usage": {"input_tokens": 11261, "output_tokens": 222, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11483}}}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-project-isolated.capture.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-project-isolated.capture.ndjson new file mode 100644 index 0000000000..c3f823d332 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-project-isolated.capture.ndjson @@ -0,0 +1,4 @@ +{"t_ms": 0, "stream": "capture", "data": {"setup_file": "/.agents/mcp_config.json", "content": {"mcpServers": {"phase0": {"serverUrl": "http://127.0.0.1:/mcp"}}}, "global_mcp_config_exists": false}} +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", "", "--dangerously-skip-permissions"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 9494, "stream": "capture", "data": {"global_mcp_config_exists": true}} +{"t_ms": 9645, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-project-isolated.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-project-isolated.stderr.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/mcp-project-isolated.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-project-isolated.stdin.ndjson new file mode 100644 index 0000000000..7907cb1235 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-project-isolated.stdin.ndjson @@ -0,0 +1 @@ +{"t_ms": 301, "stream": "stdin", "data": {"event": "user", "message": {"content": "Call the phase0 MCP echo tool with text PHASE0_MCP. If it fails report the exact error. No shell."}}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-project-isolated.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-project-isolated.stdout.ndjson new file mode 100644 index 0000000000..632daa6821 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-project-isolated.stdout.ndjson @@ -0,0 +1,31 @@ +{"t_ms": 301, "stream": "stdout", "data": {"event": "init", "conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "always-proceed"}}} +{"t_ms": 357, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 3586, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 1, "state": "DONE", "step_type": "agent_response", "duration_seconds": 3.20056914, "usage": {"input_tokens": 11979, "output_tokens": 300, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12279}}}} +{"t_ms": 3587, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 2, "state": "ACTIVE", "step_type": "tool", "tool_name": "view_file", "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/.gemini/antigravity-cli/mcp/phase0/echo.json"}}}}} +{"t_ms": 3587, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 2, "state": "DONE", "step_type": "tool", "tool_name": "view_file", "duration_seconds": 0.003339143, "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/.gemini/antigravity-cli/mcp/phase0/echo.json"}, "output": "1 lines, 184 bytes"}}}} +{"t_ms": 5350, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 3, "state": "DONE", "step_type": "agent_response", "duration_seconds": 1.765191975, "usage": {"input_tokens": 12284, "output_tokens": 61, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12345}}}} +{"t_ms": 5350, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 4, "state": "ACTIVE", "step_type": "tool", "tool_name": "call_mcp_tool", "tool_info": {"name": "call_mcp_tool", "parameters": {"Arguments": "{\"text\":\"PHASE0_MCP\"}", "ServerName": "phase0", "ToolName": "echo"}}}}} +{"t_ms": 5350, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 4, "state": "ERROR", "step_type": "tool", "tool_name": "call_mcp_tool", "duration_seconds": 0.004524751, "tool_info": {"name": "call_mcp_tool", "parameters": {"Arguments": "{\"text\":\"PHASE0_MCP\"}", "ServerName": "phase0", "ToolName": "echo"}, "error": {"type": "TOOL_ERROR", "message": "calling \"tools/call\": [\n {\n \"expected\": \"record\",\n \"code\": \"invalid_type\",\n \"path\": [\n \"params\",\n \"arguments\"\n ],\n \"message\": \"Invalid input: expected record, received string\"\n }\n]"}}}}} +{"t_ms": 7088, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 5, "state": "DONE", "step_type": "agent_response", "duration_seconds": 1.72463283, "usage": {"input_tokens": 12468, "output_tokens": 53, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12521}}}} +{"t_ms": 7088, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 6, "state": "ACTIVE", "step_type": "tool", "tool_name": "call_mcp_tool", "tool_info": {"name": "call_mcp_tool", "parameters": {"Arguments": "null", "ServerName": "phase0", "ToolName": "echo"}}}}} +{"t_ms": 7088, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 6, "state": "ERROR", "step_type": "tool", "tool_name": "call_mcp_tool", "duration_seconds": 0.00450856, "tool_info": {"name": "call_mcp_tool", "parameters": {"Arguments": "null", "ServerName": "phase0", "ToolName": "echo"}, "error": {"type": "TOOL_ERROR", "message": "calling \"tools/call\": [\n {\n \"expected\": \"record\",\n \"code\": \"invalid_type\",\n \"path\": [\n \"params\",\n \"arguments\"\n ],\n \"message\": \"Invalid input: expected record, received string\"\n }\n]"}}}}} +{"t_ms": 8903, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "The call"}}} +{"t_ms": 8979, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " to the `pha"}}} +{"t_ms": 9005, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "se0` MCP `echo` tool fai"}}} +{"t_ms": 9031, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "led with the following e"}}} +{"t_ms": 9056, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "xact error:\n"}}} +{"t_ms": 9082, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "\n"}}} +{"t_ms": 9107, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "```json\ncall"}}} +{"t_ms": 9133, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ing \"tools/call\": [\n {\n"}}} +{"t_ms": 9158, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " \"expected\": \"record\""}}} +{"t_ms": 9185, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": ",\n \"code\""}}} +{"t_ms": 9210, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": ": \"invalid_type\",\n \"path\": [\n "}}} +{"t_ms": 9235, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " \"params\""}}} +{"t_ms": 9261, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": ",\n \"arguments\"\n "}}} +{"t_ms": 9286, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "],\n "}}} +{"t_ms": 9312, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "\"message\": \""}}} +{"t_ms": 9338, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "Invalid input: expected "}}} +{"t_ms": 9363, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "record, rece"}}} +{"t_ms": 9389, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ived string\"\n }\n]\n```"}}} +{"t_ms": 9468, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "step_index": 7, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 2.395342779, "usage": {"input_tokens": 12644, "output_tokens": 93, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12737}}}} +{"t_ms": 9494, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "0ff3bbbf-5757-4c2b-95c7-8287283c5f4e", "status": "SUCCESS", "response": "The call to the `phase0` MCP `echo` tool failed with the following exact error:\n\n```json\ncalling \"tools/call\": [\n {\n \"expected\": \"record\",\n \"code\": \"invalid_type\",\n \"path\": [\n \"params\",\n \"arguments\"\n ],\n \"message\": \"Invalid input: expected record, received string\"\n }\n]\n```\n", "duration_seconds": 9.15918287, "num_turns": 1, "usage": {"input_tokens": 49375, "output_tokens": 507, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 49882}}}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-project.capture.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-project.capture.ndjson new file mode 100644 index 0000000000..041597c746 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-project.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", "", "--disable-slash-commands", "--dangerously-skip-permissions"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 30078, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-project.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-project.stderr.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/mcp-project.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-project.stdin.ndjson new file mode 100644 index 0000000000..de5df7cd48 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-project.stdin.ndjson @@ -0,0 +1 @@ +{"t_ms": 286, "stream": "stdin", "data": {"event": "user", "message": {"content": "Call the phase0 MCP echo tool with text PHASE0_MCP. Do not use shell. If unavailable say UNAVAILABLE."}}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-project.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-project.stdout.ndjson new file mode 100644 index 0000000000..323648a229 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-project.stdout.ndjson @@ -0,0 +1,15 @@ +{"t_ms": 285, "stream": "stdout", "data": {"event": "init", "conversation_id": "89273b5a-77cd-4d4b-ac80-4475b2c751d0", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "always-proceed"}}} +{"t_ms": 339, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "89273b5a-77cd-4d4b-ac80-4475b2c751d0", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 4401, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "89273b5a-77cd-4d4b-ac80-4475b2c751d0", "step_index": 1, "state": "DONE", "step_type": "agent_response", "duration_seconds": 4.075516272, "usage": {"input_tokens": 11933, "output_tokens": 393, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12326}}}} +{"t_ms": 4401, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "89273b5a-77cd-4d4b-ac80-4475b2c751d0", "step_index": 2, "state": "ACTIVE", "step_type": "tool", "tool_name": "view_file", "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/.gemini/antigravity-cli/mcp/phase0/echo.json"}}}}} +{"t_ms": 4427, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "89273b5a-77cd-4d4b-ac80-4475b2c751d0", "step_index": 2, "state": "DONE", "step_type": "tool", "tool_name": "view_file", "duration_seconds": 0.003930268, "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/.gemini/antigravity-cli/mcp/phase0/echo.json"}, "output": "1 lines, 184 bytes"}}}} +{"t_ms": 6440, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "89273b5a-77cd-4d4b-ac80-4475b2c751d0", "step_index": 3, "state": "DONE", "step_type": "agent_response", "duration_seconds": 2.013674566, "usage": {"input_tokens": 12234, "output_tokens": 63, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12297}}}} +{"t_ms": 6441, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "89273b5a-77cd-4d4b-ac80-4475b2c751d0", "step_index": 4, "state": "ACTIVE", "step_type": "tool", "tool_name": "call_mcp_tool", "tool_info": {"name": "call_mcp_tool", "parameters": {"Arguments": "{\"text\":\"PHASE0_MCP\"}", "ServerName": "phase0", "ToolName": "echo"}}}}} +{"t_ms": 6441, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "89273b5a-77cd-4d4b-ac80-4475b2c751d0", "step_index": 4, "state": "ERROR", "step_type": "tool", "tool_name": "call_mcp_tool", "duration_seconds": 0.003539628, "tool_info": {"name": "call_mcp_tool", "parameters": {"Arguments": "{\"text\":\"PHASE0_MCP\"}", "ServerName": "phase0", "ToolName": "echo"}, "error": {"type": "TOOL_ERROR", "message": "calling \"tools/call\": [\n {\n \"expected\": \"record\",\n \"code\": \"invalid_type\",\n \"path\": [\n \"params\",\n \"arguments\"\n ],\n \"message\": \"Invalid input: expected record, received string\"\n }\n]"}}}}} +{"t_ms": 9031, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "89273b5a-77cd-4d4b-ac80-4475b2c751d0", "step_index": 5, "state": "DONE", "step_type": "agent_response", "duration_seconds": 2.5843047, "usage": {"input_tokens": 12420, "output_tokens": 64, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12484}}}} +{"t_ms": 9031, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "89273b5a-77cd-4d4b-ac80-4475b2c751d0", "step_index": 6, "state": "ACTIVE", "step_type": "tool", "tool_name": "call_mcp_tool", "tool_info": {"name": "call_mcp_tool", "parameters": {"Arguments": "{\"text\": \"PHASE0_MCP\"}", "ServerName": "phase0", "ToolName": "echo"}}}}} +{"t_ms": 9031, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "89273b5a-77cd-4d4b-ac80-4475b2c751d0", "step_index": 6, "state": "ERROR", "step_type": "tool", "tool_name": "call_mcp_tool", "duration_seconds": 0.002964553, "tool_info": {"name": "call_mcp_tool", "parameters": {"Arguments": "{\"text\": \"PHASE0_MCP\"}", "ServerName": "phase0", "ToolName": "echo"}, "error": {"type": "TOOL_ERROR", "message": "calling \"tools/call\": [\n {\n \"expected\": \"record\",\n \"code\": \"invalid_type\",\n \"path\": [\n \"params\",\n \"arguments\"\n ],\n \"message\": \"Invalid input: expected record, received string\"\n }\n]"}}}}} +{"t_ms": 24041, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "89273b5a-77cd-4d4b-ac80-4475b2c751d0", "step_index": 7, "state": "DONE", "step_type": "error_message", "duration_seconds": 0}}} +{"t_ms": 29423, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "89273b5a-77cd-4d4b-ac80-4475b2c751d0", "step_index": 8, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "UNAVAILABLE"}}} +{"t_ms": 29980, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "89273b5a-77cd-4d4b-ac80-4475b2c751d0", "step_index": 8, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 1.922531311, "usage": {"input_tokens": 12607, "output_tokens": 2, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12609}}}} +{"t_ms": 29980, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "89273b5a-77cd-4d4b-ac80-4475b2c751d0", "status": "ERROR", "response": "UNAVAILABLE\n", "error": "API error (attempt 1): Error 502, Message: , Status: 502 Bad Gateway, Details: []", "duration_seconds": 29.671957293, "num_turns": 1, "usage": {"input_tokens": 49194, "output_tokens": 522, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 49716}}}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-second-add-dir.capture.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-second-add-dir.capture.ndjson new file mode 100644 index 0000000000..d42fd1a128 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-second-add-dir.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--disable-slash-commands", "--dangerously-skip-permissions", "--add-dir", "", "--add-dir", "/owned"], "cwd": "", "home": "/mcp-second-add-dir-home", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 18531, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-second-add-dir.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-second-add-dir.stderr.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/mcp-second-add-dir.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-second-add-dir.stdin.ndjson new file mode 100644 index 0000000000..eeddbbd260 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-second-add-dir.stdin.ndjson @@ -0,0 +1 @@ +{"t_ms": 308, "stream": "stdin", "data": {"event": "user", "message": {"content": "First call the phase2 MCP echo tool with text PHASE2_DISCOVERY. If unavailable say MCP_UNAVAILABLE. Then use shell pwd, read input.txt and write output.txt containing PHASE2_WRITE. Use the workspace default directory, without me supplying absolute paths. Report the exact paths used."}}} diff --git a/plugins/antigravity-provider/test/fixtures/mcp-second-add-dir.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/mcp-second-add-dir.stdout.ndjson new file mode 100644 index 0000000000..274627ad6b --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/mcp-second-add-dir.stdout.ndjson @@ -0,0 +1,71 @@ +{"t_ms": 308, "stream": "stdout", "data": {"event": "init", "conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "always-proceed"}}} +{"t_ms": 386, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 4340, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 1, "state": "DONE", "step_type": "agent_response", "duration_seconds": 3.94876824, "usage": {"input_tokens": 11996, "output_tokens": 440, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12436}}}} +{"t_ms": 4340, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 2, "state": "ACTIVE", "step_type": "tool", "tool_name": "view_file", "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/mcp-second-add-dir-home/.gemini/antigravity-cli/mcp/phase2/echo.json"}}}}} +{"t_ms": 4340, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 2, "state": "DONE", "step_type": "tool", "tool_name": "view_file", "duration_seconds": 0.003707182, "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/mcp-second-add-dir-home/.gemini/antigravity-cli/mcp/phase2/echo.json"}, "output": "1 lines, 184 bytes"}}}} +{"t_ms": 6080, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 3, "state": "DONE", "step_type": "agent_response", "duration_seconds": 1.730381429, "usage": {"input_tokens": 12306, "output_tokens": 64, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12370}}}} +{"t_ms": 6080, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 4, "state": "ACTIVE", "step_type": "tool", "tool_name": "call_mcp_tool", "tool_info": {"name": "call_mcp_tool", "parameters": {"Arguments": "{\"text\":\"PHASE2_DISCOVERY\"}", "ServerName": "phase2", "ToolName": "echo"}}}}} +{"t_ms": 6080, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 4, "state": "ERROR", "step_type": "tool", "tool_name": "call_mcp_tool", "duration_seconds": 0.005173122, "tool_info": {"name": "call_mcp_tool", "parameters": {"Arguments": "{\"text\":\"PHASE2_DISCOVERY\"}", "ServerName": "phase2", "ToolName": "echo"}, "error": {"type": "TOOL_ERROR", "message": "calling \"tools/call\": [\n {\n \"expected\": \"record\",\n \"code\": \"invalid_type\",\n \"path\": [\n \"params\",\n \"arguments\"\n ],\n \"message\": \"Invalid input: expected record, received string\"\n }\n]"}}}}} +{"t_ms": 7982, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 5, "state": "DONE", "step_type": "agent_response", "duration_seconds": 1.901210495, "usage": {"input_tokens": 12493, "output_tokens": 68, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12561}}}} +{"t_ms": 7982, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 6, "state": "ACTIVE", "step_type": "tool", "tool_name": "run_command", "tool_info": {"name": "run_command", "parameters": {"CommandLine": "pwd"}}}}} +{"t_ms": 7982, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 6, "state": "DONE", "step_type": "tool", "tool_name": "run_command", "duration_seconds": 0.006768796, "tool_info": {"name": "run_command", "parameters": {"CommandLine": "pwd"}, "output": "/owned\r\n"}}}} +{"t_ms": 9669, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 7, "state": "DONE", "step_type": "agent_response", "duration_seconds": 1.692085483, "usage": {"input_tokens": 12626, "output_tokens": 48, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12674}}}} +{"t_ms": 9669, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 8, "state": "ACTIVE", "step_type": "tool", "tool_name": "view_file", "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/owned/input.txt"}}}}} +{"t_ms": 9669, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 8, "state": "ERROR", "step_type": "tool", "tool_name": "view_file", "duration_seconds": 0.001462294, "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/owned/input.txt"}, "error": {"type": "TOOL_ERROR", "message": "declaring permissions: cortex tool view_file: convert tool call for permissions: model output error: invalid tool call error (invalid_args) failed to read file: stat /owned/input.txt: no such file or directory"}}}}} +{"t_ms": 11257, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 9, "state": "DONE", "step_type": "agent_response", "duration_seconds": 1.561062696, "usage": {"input_tokens": 12788, "output_tokens": 51, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12839}}}} +{"t_ms": 11257, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 10, "state": "ACTIVE", "step_type": "tool", "tool_name": "view_file", "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/input.txt"}}}}} +{"t_ms": 11257, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 10, "state": "DONE", "step_type": "tool", "tool_name": "view_file", "duration_seconds": 0.002866119, "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/input.txt"}, "output": "1 lines, 23 bytes"}}}} +{"t_ms": 13169, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 11, "state": "DONE", "step_type": "agent_response", "duration_seconds": 1.9176663440000001, "usage": {"input_tokens": 13010, "output_tokens": 84, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 13094}}}} +{"t_ms": 13170, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 12, "state": "ACTIVE", "step_type": "tool", "tool_name": "write_to_file", "tool_info": {"name": "write_to_file", "parameters": {"TargetFile": "/output.txt"}}}}} +{"t_ms": 13170, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 12, "state": "DONE", "step_type": "tool", "tool_name": "write_to_file", "duration_seconds": 0.003297375, "tool_info": {"name": "write_to_file", "parameters": {"TargetFile": "/output.txt"}}}}} +{"t_ms": 15284, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 13, "state": "DONE", "step_type": "agent_response", "duration_seconds": 2.118805123, "usage": {"input_tokens": 13182, "output_tokens": 84, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 13266}}}} +{"t_ms": 15284, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 14, "state": "ACTIVE", "step_type": "tool", "tool_name": "write_to_file", "tool_info": {"name": "write_to_file", "parameters": {"TargetFile": "/owned/output.txt"}}}}} +{"t_ms": 15284, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 14, "state": "DONE", "step_type": "tool", "tool_name": "write_to_file", "duration_seconds": 0.003384669, "tool_info": {"name": "write_to_file", "parameters": {"TargetFile": "/owned/output.txt"}}}}} +{"t_ms": 16695, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "### MCP"}}} +{"t_ms": 16797, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " Tool Execut"}}} +{"t_ms": 16823, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ion\n- Calling the `phase"}}} +{"t_ms": 16848, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "2` MCP `echo` tool faile"}}} +{"t_ms": 16874, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "d with an"}}} +{"t_ms": 17102, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " argument validation error from the "}}} +{"t_ms": 17128, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "server schem"}}} +{"t_ms": 17153, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "a interface:\n `MCP_UNAV"}}} +{"t_ms": 17178, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "AILABLE` (er"}}} +{"t_ms": 17204, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ror: `Invalid input: expected record"}}} +{"t_ms": 17229, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": ", received s"}}} +{"t_ms": 17255, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "tring`)\n\n---\n\n### Shell "}}} +{"t_ms": 17281, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "`pwd` & Workspace File O"}}} +{"t_ms": 17306, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "perations\n- "}}} +{"t_ms": 17331, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "**Current Working Directory (`pwd`):"}}} +{"t_ms": 17356, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "** `/tmp/agy"}}} +{"t_ms": 17382, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "-p2/owned`\n- **Read file"}}} +{"t_ms": 17407, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": ":** [input.txt](file:///w"}}} +{"t_ms": 17433, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "orkspace/inp"}}} +{"t_ms": 17459, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ut.txt) (Content: `PHASE"}}} +{"t_ms": 17484, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "2_WORKSPACE_"}}} +{"t_ms": 17509, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "MARK"}}} +{"t_ms": 17535, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ER`)\n- **Written file:**"}}} +{"t_ms": 17560, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " [output.txt"}}} +{"t_ms": 17586, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "](file:///wor"}}} +{"t_ms": 17611, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "kspace/outpu"}}} +{"t_ms": 17637, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "t"}}} +{"t_ms": 17738, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": ".txt) (and m"}}} +{"t_ms": 17764, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "irrored to [output.txt]("}}} +{"t_ms": 17789, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "file:///tmp/"}}} +{"t_ms": 17814, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "agy-p2/owned/output.txt)"}}} +{"t_ms": 17840, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": ") containing "}}} +{"t_ms": 17941, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "`PHASE2_WRITE`\n\n### Exac"}}} +{"t_ms": 17966, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "t Paths Used\n- Default C"}}} +{"t_ms": 17992, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ommand Worki"}}} +{"t_ms": 18017, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ng Directory: `/tmp/agy-"}}} +{"t_ms": 18043, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "p2/owned"}}} +{"t_ms": 18069, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "`\n- Workspac"}}} +{"t_ms": 18095, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "e Read Path: `/tmp/agy-p"}}} +{"t_ms": 18120, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "2/workspace/"}}} +{"t_ms": 18145, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "input.txt`\n- Output Writ"}}} +{"t_ms": 18171, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ten"}}} +{"t_ms": 18196, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " Paths:\n - `"}}} +{"t_ms": 18222, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "/workspace/o"}}} +{"t_ms": 18247, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "utput.txt`\n - `/tmp"}}} +{"t_ms": 18273, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "/agy-p2/owned/output.txt`"}}} +{"t_ms": 18450, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "step_index": 15, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 3.1541972400000002, "usage": {"input_tokens": 13354, "output_tokens": 262, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 13616}}}} +{"t_ms": 18450, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "2005ea86-6c72-484e-95fb-99a1c0fed68f", "status": "SUCCESS", "response": "### MCP Tool Execution\n- Calling the `phase2` MCP `echo` tool failed with an argument validation error from the server schema interface:\n `MCP_UNAVAILABLE` (error: `Invalid input: expected record, received string`)\n\n---\n\n### Shell `pwd` & Workspace File Operations\n- **Current Working Directory (`pwd`):** `/owned`\n- **Read file:** [input.txt](file:///input.txt) (Content: `PHASE2_WORKSPACE_MARKER`)\n- **Written file:** [output.txt](file:///output.txt) (and mirrored to [output.txt](file:///owned/output.txt)) containing `PHASE2_WRITE`\n\n### Exact Paths Used\n- Default Command Working Directory: `/owned`\n- Workspace Read Path: `/input.txt`\n- Output Written Paths:\n - `/output.txt`\n - `/owned/output.txt`\n", "duration_seconds": 18.127914828, "num_turns": 1, "usage": {"input_tokens": 101755, "output_tokens": 1101, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 102856}}}} diff --git a/plugins/antigravity-provider/test/fixtures/model-switch-invalid.capture.ndjson b/plugins/antigravity-provider/test/fixtures/model-switch-invalid.capture.ndjson new file mode 100644 index 0000000000..d9a08ce530 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/model-switch-invalid.capture.ndjson @@ -0,0 +1,3 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", "", "--conversation", "b966b16a-caf4-448a-bb24-0e34956022b0", "--model", "gemini-3.1-flash-lite-preview"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 4004, "stream": "capture", "data": {"timeout_waiting_for": "init", "seconds": 4}} +{"t_ms": 4005, "stream": "capture", "data": {"exit_code": 1}} diff --git a/plugins/antigravity-provider/test/fixtures/model-switch-invalid.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/model-switch-invalid.stderr.ndjson new file mode 100644 index 0000000000..244d1ac13e --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/model-switch-invalid.stderr.ndjson @@ -0,0 +1,13 @@ +{"t_ms": 296, "stream": "stderr", "data": "error: invalid model selection (--model \"gemini-3.1-flash-lite-preview\" --effort \"\"): model gemini-3.1-flash-lite-preview is not recognized as a known model or custom model in settings"} +{"t_ms": 296, "stream": "stderr", "data": "Available models:"} +{"t_ms": 296, "stream": "stderr", "data": " Gemini 3.8 Flash (High)"} +{"t_ms": 296, "stream": "stderr", "data": " Gemini 3.8 Flash (Medium)"} +{"t_ms": 296, "stream": "stderr", "data": " Gemini 3.8 Flash (Low)"} +{"t_ms": 296, "stream": "stderr", "data": " Gemini 3.7 Flash (High)"} +{"t_ms": 296, "stream": "stderr", "data": " Gemini 3.7 Flash (Medium)"} +{"t_ms": 296, "stream": "stderr", "data": " Gemini 3.7 Flash (Low)"} +{"t_ms": 296, "stream": "stderr", "data": " Gemini 3.6 Flash (High)"} +{"t_ms": 296, "stream": "stderr", "data": " Gemini 3.6 Flash (Medium)"} +{"t_ms": 296, "stream": "stderr", "data": " Gemini 3.6 Flash (Low)"} +{"t_ms": 297, "stream": "stderr", "data": " Gemini 3.1 Pro (High)"} +{"t_ms": 297, "stream": "stderr", "data": " Gemini 3.1 Pro (Low)"} diff --git a/plugins/antigravity-provider/test/fixtures/model-switch-invalid.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/model-switch-invalid.stdin.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/model-switch-invalid.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/model-switch-invalid.stdout.ndjson new file mode 100644 index 0000000000..1ad10e7494 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/model-switch-invalid.stdout.ndjson @@ -0,0 +1 @@ +{"t_ms": 297, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "", "status": "ERROR", "response": "", "error": "invalid model selection (--model \"gemini-3.1-flash-lite-preview\" --effort \"\"): model gemini-3.1-flash-lite-preview is not recognized as a known model or custom model in settings\nAvailable models:\n Gemini 3.8 Flash (High)\n Gemini 3.8 Flash (Medium)\n Gemini 3.8 Flash (Low)\n Gemini 3.7 Flash (High)\n Gemini 3.7 Flash (Medium)\n Gemini 3.7 Flash (Low)\n Gemini 3.6 Flash (High)\n Gemini 3.6 Flash (Medium)\n Gemini 3.6 Flash (Low)\n Gemini 3.1 Pro (High)\n Gemini 3.1 Pro (Low)", "duration_seconds": 0, "num_turns": 0, "usage": {"input_tokens": 0, "output_tokens": 0, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 0}}}} diff --git a/plugins/antigravity-provider/test/fixtures/model-switch-network.capture.ndjson b/plugins/antigravity-provider/test/fixtures/model-switch-network.capture.ndjson new file mode 100644 index 0000000000..243104c416 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/model-switch-network.capture.ndjson @@ -0,0 +1,3 @@ +{"t_ms": 1, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--conversation", "b966b16a-caf4-448a-bb24-0e34956022b0", "--model", "gemini-3.8-flash-low"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 2405, "stream": "capture", "data": {"translator_http": {"request": 6, "path": "/v1beta/models/gemini-3.8-flash:streamGenerateContent?alt=sse", "model": "google/gemini-3.7-flash", "roles": ["system", "user", "assistant", "user", "assistant", "user", "user", "assistant", "user", "user", "assistant", "user", "user"], "tools": ["view_file", "run_command", "manage_task", "send_message", "schedule", "invoke_subagent", "define_subagent", "manage_subagents", "write_to_file", "replace_file_content", "generate_image", "read_url_content", "search_web", "ask_question"], "usage": {"prompt_tokens": 12105, "completion_tokens": 12, "total_tokens": 12117, "cost": 0.00912375, "is_byok": false, "prompt_tokens_details": {"cached_tokens": 0, "cache_write_tokens": 0, "audio_tokens": 0, "video_tokens": 0}, "cost_details": {"upstream_inference_cost": 0.00912375, "upstream_inference_prompt_cost": 0.00907875, "upstream_inference_completions_cost": 4.5e-05}, "completion_tokens_details": {"reasoning_tokens": 0, "image_tokens": 0, "audio_tokens": 0}}, "toolCalls": [], "status": 200}}} +{"t_ms": 2572, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/model-switch-network.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/model-switch-network.stderr.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/model-switch-network.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/model-switch-network.stdin.ndjson new file mode 100644 index 0000000000..eb33e7913d --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/model-switch-network.stdin.ndjson @@ -0,0 +1 @@ +{"t_ms": 439, "stream": "stdin", "data": {"event": "user", "message": {"content": "What secret word did I give you? No tools."}}} diff --git a/plugins/antigravity-provider/test/fixtures/model-switch-network.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/model-switch-network.stdout.ndjson new file mode 100644 index 0000000000..e1f755f3f2 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/model-switch-network.stdout.ndjson @@ -0,0 +1,8 @@ +{"t_ms": 439, "stream": "stdout", "data": {"event": "init", "conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "init": {"model": "gemini-3.8-flash-low", "cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "request-review"}}} +{"t_ms": 560, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 10, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 560, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 11, "state": "DONE", "step_type": "system_message", "duration_seconds": 9.9025e-05}}} +{"t_ms": 2045, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 12, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "The secret"}}} +{"t_ms": 2070, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 12, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " word you ga"}}} +{"t_ms": 2096, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 12, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ve me is **COBALT**."}}} +{"t_ms": 2405, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 12, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 1.873175086, "usage": {"input_tokens": 12105, "output_tokens": 12, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12117}}}} +{"t_ms": 2405, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "status": "SUCCESS", "response": "The secret word you gave me is **COBALT**.\n", "duration_seconds": 282.68791325, "num_turns": 5, "usage": {"input_tokens": 58633, "output_tokens": 505, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 59138}}}} diff --git a/plugins/antigravity-provider/test/fixtures/model-switch-resume.capture.ndjson b/plugins/antigravity-provider/test/fixtures/model-switch-resume.capture.ndjson new file mode 100644 index 0000000000..961223783a --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/model-switch-resume.capture.ndjson @@ -0,0 +1,3 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", "", "--conversation", "b966b16a-caf4-448a-bb24-0e34956022b0", "--model", "gemini-3.1-flash-high"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 4004, "stream": "capture", "data": {"timeout_waiting_for": "init", "seconds": 4}} +{"t_ms": 4004, "stream": "capture", "data": {"exit_code": 1}} diff --git a/plugins/antigravity-provider/test/fixtures/model-switch-resume.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/model-switch-resume.stderr.ndjson new file mode 100644 index 0000000000..2666337e77 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/model-switch-resume.stderr.ndjson @@ -0,0 +1,13 @@ +{"t_ms": 239, "stream": "stderr", "data": "error: invalid model selection (--model \"gemini-3.1-flash-high\" --effort \"\"): model gemini-3.1-flash-high is not recognized as a known model or custom model in settings"} +{"t_ms": 239, "stream": "stderr", "data": "Available models:"} +{"t_ms": 239, "stream": "stderr", "data": " Gemini 3.8 Flash (High)"} +{"t_ms": 239, "stream": "stderr", "data": " Gemini 3.8 Flash (Medium)"} +{"t_ms": 239, "stream": "stderr", "data": " Gemini 3.8 Flash (Low)"} +{"t_ms": 239, "stream": "stderr", "data": " Gemini 3.7 Flash (High)"} +{"t_ms": 239, "stream": "stderr", "data": " Gemini 3.7 Flash (Medium)"} +{"t_ms": 239, "stream": "stderr", "data": " Gemini 3.7 Flash (Low)"} +{"t_ms": 239, "stream": "stderr", "data": " Gemini 3.6 Flash (High)"} +{"t_ms": 239, "stream": "stderr", "data": " Gemini 3.6 Flash (Medium)"} +{"t_ms": 239, "stream": "stderr", "data": " Gemini 3.6 Flash (Low)"} +{"t_ms": 239, "stream": "stderr", "data": " Gemini 3.1 Pro (High)"} +{"t_ms": 239, "stream": "stderr", "data": " Gemini 3.1 Pro (Low)"} diff --git a/plugins/antigravity-provider/test/fixtures/model-switch-resume.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/model-switch-resume.stdin.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/model-switch-resume.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/model-switch-resume.stdout.ndjson new file mode 100644 index 0000000000..3556e4c951 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/model-switch-resume.stdout.ndjson @@ -0,0 +1 @@ +{"t_ms": 239, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "", "status": "ERROR", "response": "", "error": "invalid model selection (--model \"gemini-3.1-flash-high\" --effort \"\"): model gemini-3.1-flash-high is not recognized as a known model or custom model in settings\nAvailable models:\n Gemini 3.8 Flash (High)\n Gemini 3.8 Flash (Medium)\n Gemini 3.8 Flash (Low)\n Gemini 3.7 Flash (High)\n Gemini 3.7 Flash (Medium)\n Gemini 3.7 Flash (Low)\n Gemini 3.6 Flash (High)\n Gemini 3.6 Flash (Medium)\n Gemini 3.6 Flash (Low)\n Gemini 3.1 Pro (High)\n Gemini 3.1 Pro (Low)", "duration_seconds": 0, "num_turns": 0, "usage": {"input_tokens": 0, "output_tokens": 0, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 0}}}} diff --git a/plugins/antigravity-provider/test/fixtures/model-switch-valid.capture.ndjson b/plugins/antigravity-provider/test/fixtures/model-switch-valid.capture.ndjson new file mode 100644 index 0000000000..9d44fc439e --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/model-switch-valid.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--conversation", "b966b16a-caf4-448a-bb24-0e34956022b0", "--model", "gemini-3.8-flash-low"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 4608, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/model-switch-valid.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/model-switch-valid.stderr.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/model-switch-valid.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/model-switch-valid.stdin.ndjson new file mode 100644 index 0000000000..f1fb9508ac --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/model-switch-valid.stdin.ndjson @@ -0,0 +1 @@ +{"t_ms": 296, "stream": "stdin", "data": {"event": "user", "message": {"content": "What secret word did I give you? No tools."}}} diff --git a/plugins/antigravity-provider/test/fixtures/model-switch-valid.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/model-switch-valid.stdout.ndjson new file mode 100644 index 0000000000..70dbf755eb --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/model-switch-valid.stdout.ndjson @@ -0,0 +1,7 @@ +{"t_ms": 296, "stream": "stdout", "data": {"event": "init", "conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "init": {"model": "gemini-3.8-flash-low", "cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "request-review"}}} +{"t_ms": 342, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 7, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 342, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 8, "state": "DONE", "step_type": "system_message", "duration_seconds": 4.783e-05}}} +{"t_ms": 3891, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "The secret"}}} +{"t_ms": 4018, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " word you gave me is **COBALT**."}}} +{"t_ms": 4473, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 9, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 4.143724393, "usage": {"input_tokens": 11846, "output_tokens": 12, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11858}}}} +{"t_ms": 4499, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "status": "SUCCESS", "response": "The secret word you gave me is **COBALT**.\n", "duration_seconds": 129.476794036, "num_turns": 4, "usage": {"input_tokens": 46528, "output_tokens": 493, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 47021}}}} diff --git a/plugins/antigravity-provider/test/fixtures/models-api-key.capture.ndjson b/plugins/antigravity-provider/test/fixtures/models-api-key.capture.ndjson new file mode 100644 index 0000000000..2db201ce59 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/models-api-key.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "models"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 360, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/models-api-key.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/models-api-key.stderr.ndjson new file mode 100644 index 0000000000..31ed588249 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/models-api-key.stderr.ndjson @@ -0,0 +1 @@ +{"t_ms": 247, "stream": "stderr", "data": "Fetching available models..."} diff --git a/plugins/antigravity-provider/test/fixtures/models-api-key.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/models-api-key.stdin.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/models-api-key.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/models-api-key.stdout.ndjson new file mode 100644 index 0000000000..ece429fc4b --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/models-api-key.stdout.ndjson @@ -0,0 +1,11 @@ +{"t_ms": 248, "stream": "stdout", "data": "gemini-3.8-flash-high\tGemini 3.8 Flash (High)"} +{"t_ms": 248, "stream": "stdout", "data": "gemini-3.8-flash-medium\tGemini 3.8 Flash (Medium)"} +{"t_ms": 248, "stream": "stdout", "data": "gemini-3.8-flash-low\tGemini 3.8 Flash (Low)"} +{"t_ms": 248, "stream": "stdout", "data": "gemini-3.7-flash-high\tGemini 3.7 Flash (High)"} +{"t_ms": 248, "stream": "stdout", "data": "gemini-3.7-flash-medium\tGemini 3.7 Flash (Medium)"} +{"t_ms": 248, "stream": "stdout", "data": "gemini-3.7-flash-low\tGemini 3.7 Flash (Low)"} +{"t_ms": 248, "stream": "stdout", "data": "gemini-3.6-flash-high\tGemini 3.6 Flash (High)"} +{"t_ms": 248, "stream": "stdout", "data": "gemini-3.6-flash-medium\tGemini 3.6 Flash (Medium)"} +{"t_ms": 248, "stream": "stdout", "data": "gemini-3.6-flash-low\tGemini 3.6 Flash (Low)"} +{"t_ms": 248, "stream": "stdout", "data": "gemini-3.1-pro-high\tGemini 3.1 Pro (High)"} +{"t_ms": 248, "stream": "stdout", "data": "gemini-3.1-pro-low\tGemini 3.1 Pro (Low)"} diff --git a/plugins/antigravity-provider/test/fixtures/models-unauthenticated.capture.ndjson b/plugins/antigravity-provider/test/fixtures/models-unauthenticated.capture.ndjson new file mode 100644 index 0000000000..b91f7dfb82 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/models-unauthenticated.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "models"], "cwd": "", "home": "", "settings": null, "env": {}}} +{"t_ms": 567, "stream": "capture", "data": {"exit_code": 1}} diff --git a/plugins/antigravity-provider/test/fixtures/models-unauthenticated.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/models-unauthenticated.stderr.ndjson new file mode 100644 index 0000000000..0bcbc4745c --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/models-unauthenticated.stderr.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 404, "stream": "stderr", "data": "Fetching available models..."} +{"t_ms": 404, "stream": "stderr", "data": "Error: Please sign in to view available models. Launch the CLI without arguments to sign in."} diff --git a/plugins/antigravity-provider/test/fixtures/models-unauthenticated.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/models-unauthenticated.stdin.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/models-unauthenticated.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/models-unauthenticated.stdout.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/multi-turn-reasoning.capture.ndjson b/plugins/antigravity-provider/test/fixtures/multi-turn-reasoning.capture.ndjson new file mode 100644 index 0000000000..56e04c0abe --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/multi-turn-reasoning.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", "", "--disable-slash-commands"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 6732, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/multi-turn-reasoning.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/multi-turn-reasoning.stderr.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/multi-turn-reasoning.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/multi-turn-reasoning.stdin.ndjson new file mode 100644 index 0000000000..4210a3f1b6 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/multi-turn-reasoning.stdin.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 304, "stream": "stdin", "data": {"event": "user", "message": {"role": "user", "content": "Remember the secret word COBALT. Solve 17*23 with reasoning, then reply with the answer. No tools."}}} +{"t_ms": 4752, "stream": "stdin", "data": {"event": "user", "message": {"role": "user", "content": "What secret word did I give you? Reply with only that word. No tools."}}} diff --git a/plugins/antigravity-provider/test/fixtures/multi-turn-reasoning.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/multi-turn-reasoning.stdout.ndjson new file mode 100644 index 0000000000..dd2a346d75 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/multi-turn-reasoning.stdout.ndjson @@ -0,0 +1,51 @@ +{"t_ms": 304, "stream": "stdout", "data": {"event": "init", "conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "request-review"}}} +{"t_ms": 332, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 3128, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "I have noted"}}} +{"t_ms": 3153, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " and remembe"}}} +{"t_ms": 3178, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "red the secret word **CO"}}} +{"t_ms": 3203, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "BALT**.\n\n###"}}} +{"t_ms": 3229, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " Reasoning"}}} +{"t_ms": 3280, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " for $17 \\ti"}}} +{"t_ms": 3306, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "mes 23$:\n\nWe can solve $"}}} +{"t_ms": 3332, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "17 \\times 23$ using the "}}} +{"t_ms": 3357, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "difference o"}}} +{"t_ms": 3382, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "f"}}} +{"t_ms": 3459, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " squares ide"}}} +{"t_ms": 3484, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ntity, $(a - b)(a + b) ="}}} +{"t_ms": 3510, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " a^2 - b^2$:\n\n1. Rewrite"}}} +{"t_ms": 3535, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " the"}}} +{"t_ms": 3688, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " terms centered around 20:\n $$17 ="}}} +{"t_ms": 3713, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " 20 - 3$$\n "}}} +{"t_ms": 3738, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " $$23"}}} +{"t_ms": 3814, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " = 20 + 3$$\n\n2. Apply th"}}} +{"t_ms": 3840, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "e difference"}}} +{"t_ms": 3865, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " of squares formula:\n "}}} +{"t_ms": 3891, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "$$17 \\times "}}} +{"t_ms": 3917, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "23 = (20 - 3)(20 + 3) = 2"}}} +{"t_ms": 3942, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "0^2 - 3"}}} +{"t_ms": 4069, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "^2$$\n\n3. Cal"}}} +{"t_ms": 4094, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "culate the squares:\n $"}}} +{"t_ms": 4119, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "$20^2 = 400$"}}} +{"t_ms": 4145, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "$\n "}}} +{"t_ms": 4170, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "$$3^2 = 9$$\n"}}} +{"t_ms": 4221, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "\n4. Subtract"}}} +{"t_ms": 4246, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": ":\n $$400 -"}}} +{"t_ms": 4271, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " 9 = 3"}}} +{"t_ms": 4323, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "91$$\n\n*(Alte"}}} +{"t_ms": 4348, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "rnatively, v"}}} +{"t_ms": 4398, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ia partial p"}}} +{"t_ms": 4424, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "roducts: $17"}}} +{"t_ms": 4474, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " \\times 20 ="}}} +{"t_ms": 4499, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " 340$, and $"}}} +{"t_ms": 4550, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "17 \\times 3 "}}} +{"t_ms": 4575, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "= 51$; then "}}} +{"t_ms": 4600, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "$340 + 51 = "}}} +{"t_ms": 4650, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "391$.)*\n\n---"}}} +{"t_ms": 4675, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "\n\n**Answer:*"}}} +{"t_ms": 4726, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "* **391**"}}} +{"t_ms": 4752, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 1, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 4.392588837, "usage": {"input_tokens": 11263, "output_tokens": 466, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11729}}}} +{"t_ms": 4752, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "status": "SUCCESS", "response": "I have noted and remembered the secret word **COBALT**.\n\n### Reasoning for $17 \\times 23$:\n\nWe can solve $17 \\times 23$ using the difference of squares identity, $(a - b)(a + b) = a^2 - b^2$:\n\n1. Rewrite the terms centered around 20:\n $$17 = 20 - 3$$\n $$23 = 20 + 3$$\n\n2. Apply the difference of squares formula:\n $$17 \\times 23 = (20 - 3)(20 + 3) = 20^2 - 3^2$$\n\n3. Calculate the squares:\n $$20^2 = 400$$\n $$3^2 = 9$$\n\n4. Subtract:\n $$400 - 9 = 391$$\n\n*(Alternatively, via partial products: $17 \\times 20 = 340$, and $17 \\times 3 = 51$; then $340 + 51 = 391$.)*\n\n---\n\n**Answer:** **391**\n", "duration_seconds": 4.414417492, "num_turns": 1, "usage": {"input_tokens": 11263, "output_tokens": 466, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11729}}}} +{"t_ms": 4785, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 2, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 6345, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "COBALT"}}} +{"t_ms": 6573, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 3, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 1.784280065, "usage": {"input_tokens": 11604, "output_tokens": 3, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11607}}}} +{"t_ms": 6573, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "status": "SUCCESS", "response": "COBALT\n", "duration_seconds": 6.242840924, "num_turns": 2, "usage": {"input_tokens": 22867, "output_tokens": 469, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 23336}}}} diff --git a/plugins/antigravity-provider/test/fixtures/not-signed-in.capture.ndjson b/plugins/antigravity-provider/test/fixtures/not-signed-in.capture.ndjson new file mode 100644 index 0000000000..a77dc1e4ee --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/not-signed-in.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json"], "cwd": "", "home": "", "settings": null, "env": {}}} +{"t_ms": 520, "stream": "capture", "data": {"exit_code": 1}} diff --git a/plugins/antigravity-provider/test/fixtures/not-signed-in.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/not-signed-in.stderr.ndjson new file mode 100644 index 0000000000..9304e8b2ee --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/not-signed-in.stderr.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 360, "stream": "stderr", "data": "Error: authentication required. Run 'agy' to log in, then retry."} +{"t_ms": 360, "stream": "stderr", "data": "error: authentication failed or timed out"} diff --git a/plugins/antigravity-provider/test/fixtures/not-signed-in.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/not-signed-in.stdin.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/not-signed-in.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/not-signed-in.stdout.ndjson new file mode 100644 index 0000000000..760f2dbe68 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/not-signed-in.stdout.ndjson @@ -0,0 +1 @@ +{"t_ms": 360, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "", "status": "ERROR", "response": "", "error": "authentication failed or timed out", "duration_seconds": 0, "num_turns": 0, "usage": {"input_tokens": 0, "output_tokens": 0, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 0}}}} diff --git a/plugins/antigravity-provider/test/fixtures/parity-inspection.ndjson b/plugins/antigravity-provider/test/fixtures/parity-inspection.ndjson new file mode 100644 index 0000000000..18c16ef484 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/parity-inspection.ndjson @@ -0,0 +1,10 @@ +{"t_ms": 0, "stream": "inspection", "data": {"type": "configuration", "file": "/owned/.agents/mcp_config.json", "content": {"mcpServers": {"phase2": {"serverUrl": "http://127.0.0.1:/mcp"}}}}} +{"t_ms": 0, "stream": "inspection", "data": {"type": "isolation", "global_mcp_config_exists": {"mcp-second-add-dir": true, "mcp-owned-cwd": true, "mcp-owned-first-add-dir": true, "mcp-agent-file": true, "mcp-agent-definition": true, "mcp-agent-discovered": true, "image-file-external": true}}} +{"t_ms": 0, "stream": "inspection", "data": {"type": "agent_definition", "file": "/owned/agent.md", "content": "---\nname: phase2\ndescription: Session adapter\nmcpServers: {\"phase2\": {\"serverUrl\": \"http://127.0.0.1:/mcp\"}}\n---\nWork in the current workspace.\n"}} +{"t_ms": 0, "stream": "inspection", "data": {"type": "agent_definition", "file": "/owned/.agents/agents/phase2.md", "content": "---\nname: phase2\ndescription: Session adapter\nmainAgent: true\nworkdir: \nmcpServers: {\"phase2\": {\"serverUrl\": \"http://127.0.0.1:/mcp\"}}\n---\n# Identity\nWork in the current workspace.\n"}} +{"t_ms": 0, "stream": "inspection", "data": {"type": "agent_manifest", "file": "/owned/.agents/agents.json", "content": {"entries": [{"path": "/owned/.agents/agents"}]}}} +{"t_ms": 0, "stream": "inspection", "data": {"type": "native_log", "capture": "mcp-agent-file", "line": "W0929 21:29:08.043649 1 session.go:94] Agent \"/owned/agent.md\" not found, falling back to default"}} +{"t_ms": 0, "stream": "inspection", "data": {"type": "native_log", "capture": "mcp-agent-definition", "line": "W0929 21:30:59.337788 1 session.go:94] Agent \"phase2\" not found, falling back to default"}} +{"t_ms": 0, "stream": "inspection", "data": {"type": "native_log", "capture": "mcp-agent-discovered", "line": "W0929 21:33:13.643983 1 session.go:94] Agent \"phase2\" not found, falling back to default"}} +{"t_ms": 0, "stream": "inspection", "data": {"type": "translator_image_part", "function": "view_file", "location": "functionResponse.parts[].inlineData", "mimeType": "image/png", "forwarded": true}} +{"t_ms": 0, "stream": "inspection", "data": {"type": "image", "fixture": "image-file-external.png", "sha256": "abd9ec3ff865aa30218d6184eadff4c0fc1c55d6ada251909d6b4554d64d5336", "expected": "Red triangle on the left, blue circle on the lower right, white background."}} diff --git a/plugins/antigravity-provider/test/fixtures/project-skill.capture.ndjson b/plugins/antigravity-provider/test/fixtures/project-skill.capture.ndjson new file mode 100644 index 0000000000..c7e091ebe3 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/project-skill.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", "", "--disable-slash-commands"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 5673, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/project-skill.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/project-skill.stderr.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/project-skill.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/project-skill.stdin.ndjson new file mode 100644 index 0000000000..2380ec6f8e --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/project-skill.stdin.ndjson @@ -0,0 +1 @@ +{"t_ms": 361, "stream": "stdin", "data": {"event": "user", "message": {"content": "Use the phase0-marker skill now."}}} diff --git a/plugins/antigravity-provider/test/fixtures/project-skill.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/project-skill.stdout.ndjson new file mode 100644 index 0000000000..d2a8a0b936 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/project-skill.stdout.ndjson @@ -0,0 +1,9 @@ +{"t_ms": 361, "stream": "stdout", "data": {"event": "init", "conversation_id": "a145f6cf-b974-4ddb-b7eb-58bca63a0092", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "request-review"}}} +{"t_ms": 415, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a145f6cf-b974-4ddb-b7eb-58bca63a0092", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 3806, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a145f6cf-b974-4ddb-b7eb-58bca63a0092", "step_index": 1, "state": "DONE", "step_type": "agent_response", "duration_seconds": 3.389284511, "usage": {"input_tokens": 11270, "output_tokens": 246, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11516}}}} +{"t_ms": 3806, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a145f6cf-b974-4ddb-b7eb-58bca63a0092", "step_index": 2, "state": "ACTIVE", "step_type": "tool", "tool_name": "view_file", "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/.agents/skills/phase0-marker/SKILL.md"}}}}} +{"t_ms": 3806, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a145f6cf-b974-4ddb-b7eb-58bca63a0092", "step_index": 2, "state": "DONE", "step_type": "tool", "tool_name": "view_file", "duration_seconds": 0.003649634, "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/.agents/skills/phase0-marker/SKILL.md"}, "output": "6 lines, 163 bytes"}}}} +{"t_ms": 5190, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a145f6cf-b974-4ddb-b7eb-58bca63a0092", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "PHASE"}}} +{"t_ms": 5216, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a145f6cf-b974-4ddb-b7eb-58bca63a0092", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "0_SKILL_MARKER"}}} +{"t_ms": 5572, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a145f6cf-b974-4ddb-b7eb-58bca63a0092", "step_index": 3, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 1.7500252870000002, "usage": {"input_tokens": 11562, "output_tokens": 9, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11571}}}} +{"t_ms": 5572, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "a145f6cf-b974-4ddb-b7eb-58bca63a0092", "status": "SUCCESS", "response": "PHASE0_SKILL_MARKER\n", "duration_seconds": 5.185824959, "num_turns": 1, "usage": {"input_tokens": 22832, "output_tokens": 255, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 23087}}}} diff --git a/plugins/antigravity-provider/test/fixtures/question-auto-denial.capture.ndjson b/plugins/antigravity-provider/test/fixtures/question-auto-denial.capture.ndjson new file mode 100644 index 0000000000..f2e339bb07 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/question-auto-denial.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", ""], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 7149, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/question-auto-denial.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/question-auto-denial.stderr.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/question-auto-denial.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/question-auto-denial.stdin.ndjson new file mode 100644 index 0000000000..b53fbe5176 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/question-auto-denial.stdin.ndjson @@ -0,0 +1 @@ +{"t_ms": 298, "stream": "stdin", "data": {"event": "user", "message": {"content": "Use ask_question to ask me which color I prefer, red or blue. Do not choose for me."}}} diff --git a/plugins/antigravity-provider/test/fixtures/question-auto-denial.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/question-auto-denial.stdout.ndjson new file mode 100644 index 0000000000..352ea75867 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/question-auto-denial.stdout.ndjson @@ -0,0 +1,14 @@ +{"t_ms": 298, "stream": "stdout", "data": {"event": "init", "conversation_id": "e81b4d91-ca0f-4bf9-9841-d44ec1f48997", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "request-review"}}} +{"t_ms": 325, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e81b4d91-ca0f-4bf9-9841-d44ec1f48997", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 4334, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e81b4d91-ca0f-4bf9-9841-d44ec1f48997", "step_index": 1, "state": "DONE", "step_type": "agent_response", "duration_seconds": 3.994201944, "usage": {"input_tokens": 11258, "output_tokens": 297, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11555}}}} +{"t_ms": 4334, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e81b4d91-ca0f-4bf9-9841-d44ec1f48997", "step_index": 2, "state": "DONE", "step_type": "unknown", "duration_seconds": 0.00288314}}} +{"t_ms": 6347, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e81b4d91-ca0f-4bf9-9841-d44ec1f48997", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "You"}}} +{"t_ms": 6676, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e81b4d91-ca0f-4bf9-9841-d44ec1f48997", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " skipped ans"}}} +{"t_ms": 6702, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e81b4d91-ca0f-4bf9-9841-d44ec1f48997", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "wering the question. Let"}}} +{"t_ms": 6728, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e81b4d91-ca0f-4bf9-9841-d44ec1f48997", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " me know if you'd like t"}}} +{"t_ms": 6753, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e81b4d91-ca0f-4bf9-9841-d44ec1f48997", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "o choose a c"}}} +{"t_ms": 6778, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e81b4d91-ca0f-4bf9-9841-d44ec1f48997", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "olor or if there's anyth"}}} +{"t_ms": 6804, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e81b4d91-ca0f-4bf9-9841-d44ec1f48997", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ing else I can help you "}}} +{"t_ms": 6829, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e81b4d91-ca0f-4bf9-9841-d44ec1f48997", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "with!"}}} +{"t_ms": 7057, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e81b4d91-ca0f-4bf9-9841-d44ec1f48997", "step_index": 3, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 2.727510169, "usage": {"input_tokens": 11366, "output_tokens": 31, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11397}}}} +{"t_ms": 7057, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "e81b4d91-ca0f-4bf9-9841-d44ec1f48997", "status": "SUCCESS", "response": "You skipped answering the question. Let me know if you'd like to choose a color or if there's anything else I can help you with!\n", "duration_seconds": 6.748385314, "num_turns": 1, "usage": {"input_tokens": 22624, "output_tokens": 328, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 22952}}}} diff --git a/plugins/antigravity-provider/test/fixtures/resume-after-interrupt.capture.ndjson b/plugins/antigravity-provider/test/fixtures/resume-after-interrupt.capture.ndjson new file mode 100644 index 0000000000..381dc3897e --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/resume-after-interrupt.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--conversation", "a2c2132f-0f10-400b-a142-74f82230f449"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 2531, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/resume-after-interrupt.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/resume-after-interrupt.stderr.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/resume-after-interrupt.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/resume-after-interrupt.stdin.ndjson new file mode 100644 index 0000000000..40ea297e7f --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/resume-after-interrupt.stdin.ndjson @@ -0,0 +1 @@ +{"t_ms": 357, "stream": "stdin", "data": {"event": "user", "message": {"content": "Reply with PHASE0_RECOVERED. No tools."}}} diff --git a/plugins/antigravity-provider/test/fixtures/resume-after-interrupt.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/resume-after-interrupt.stdout.ndjson new file mode 100644 index 0000000000..8b3e527418 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/resume-after-interrupt.stdout.ndjson @@ -0,0 +1,7 @@ +{"t_ms": 357, "stream": "stdout", "data": {"event": "init", "conversation_id": "a2c2132f-0f10-400b-a142-74f82230f449", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "request-review"}}} +{"t_ms": 386, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a2c2132f-0f10-400b-a142-74f82230f449", "step_index": 2, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 412, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a2c2132f-0f10-400b-a142-74f82230f449", "step_index": 3, "state": "DONE", "step_type": "system_message", "duration_seconds": 6.4942e-05}}} +{"t_ms": 2228, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a2c2132f-0f10-400b-a142-74f82230f449", "step_index": 4, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "PHASE0_RECOV"}}} +{"t_ms": 2253, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a2c2132f-0f10-400b-a142-74f82230f449", "step_index": 4, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ERED"}}} +{"t_ms": 2380, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a2c2132f-0f10-400b-a142-74f82230f449", "step_index": 4, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 1.9710905699999999, "usage": {"input_tokens": 11524, "output_tokens": 65, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11589}}}} +{"t_ms": 2380, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "a2c2132f-0f10-400b-a142-74f82230f449", "status": "SUCCESS", "response": "PHASE0_RECOVERED\n", "duration_seconds": 216.301687963, "num_turns": 2, "usage": {"input_tokens": 11524, "output_tokens": 65, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11589}}}} diff --git a/plugins/antigravity-provider/test/fixtures/resume.capture.ndjson b/plugins/antigravity-provider/test/fixtures/resume.capture.ndjson new file mode 100644 index 0000000000..4caec009da --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/resume.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", "", "--disable-slash-commands", "--conversation", "b966b16a-caf4-448a-bb24-0e34956022b0"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 2317, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/resume.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/resume.stderr.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/resume.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/resume.stdin.ndjson new file mode 100644 index 0000000000..e91cc571bd --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/resume.stdin.ndjson @@ -0,0 +1 @@ +{"t_ms": 288, "stream": "stdin", "data": {"event": "user", "message": {"role": "user", "content": "What secret word did I give you? No tools."}}} diff --git a/plugins/antigravity-provider/test/fixtures/resume.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/resume.stdout.ndjson new file mode 100644 index 0000000000..d685e83cfb --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/resume.stdout.ndjson @@ -0,0 +1,8 @@ +{"t_ms": 288, "stream": "stdout", "data": {"event": "init", "conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "request-review"}}} +{"t_ms": 333, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 4, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 333, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 5, "state": "DONE", "step_type": "system_message", "duration_seconds": 4.3562e-05}}} +{"t_ms": 1969, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 6, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "The secret"}}} +{"t_ms": 2045, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 6, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " word you gave me is **C"}}} +{"t_ms": 2070, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 6, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "OBALT**."}}} +{"t_ms": 2172, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "step_index": 6, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 1.821180269, "usage": {"input_tokens": 11815, "output_tokens": 12, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11827}}}} +{"t_ms": 2172, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "status": "SUCCESS", "response": "The secret word you gave me is **COBALT**.\n", "duration_seconds": 8.574573367, "num_turns": 3, "usage": {"input_tokens": 34682, "output_tokens": 481, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 35163}}}} diff --git a/plugins/antigravity-provider/test/fixtures/schema-prompt.capture.ndjson b/plugins/antigravity-provider/test/fixtures/schema-prompt.capture.ndjson new file mode 100644 index 0000000000..9964112608 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/schema-prompt.capture.ndjson @@ -0,0 +1,3 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 30706, "stream": "capture", "data": {"timeout_waiting_for": "result", "seconds": 30}} +{"t_ms": 30794, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/schema-prompt.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/schema-prompt.stderr.ndjson new file mode 100644 index 0000000000..0a830c1e99 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/schema-prompt.stderr.ndjson @@ -0,0 +1 @@ +{"t_ms": 677, "stream": "stderr", "data": "warning: ignoring unsupported stream input message event \"prompt\""} diff --git a/plugins/antigravity-provider/test/fixtures/schema-prompt.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/schema-prompt.stdin.ndjson new file mode 100644 index 0000000000..2831eab45f --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/schema-prompt.stdin.ndjson @@ -0,0 +1 @@ +{"t_ms": 677, "stream": "stdin", "data": {"event": "prompt", "prompt": "Reply PHASE0_HELLO."}} diff --git a/plugins/antigravity-provider/test/fixtures/schema-prompt.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/schema-prompt.stdout.ndjson new file mode 100644 index 0000000000..1b83a4d41e --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/schema-prompt.stdout.ndjson @@ -0,0 +1 @@ +{"t_ms": 676, "stream": "stdout", "data": {"event": "init", "conversation_id": "11a830e0-80b1-4f55-ab34-0670be20a722", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "request-review"}}} diff --git a/plugins/antigravity-provider/test/fixtures/schema-user-message.capture.ndjson b/plugins/antigravity-provider/test/fixtures/schema-user-message.capture.ndjson new file mode 100644 index 0000000000..8db1f98058 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/schema-user-message.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 490, "stream": "capture", "data": {"exit_code": 1}} diff --git a/plugins/antigravity-provider/test/fixtures/schema-user-message.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/schema-user-message.stderr.ndjson new file mode 100644 index 0000000000..30d0fa5adb --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/schema-user-message.stderr.ndjson @@ -0,0 +1 @@ +{"t_ms": 386, "stream": "stderr", "data": "error: failed to decode stream input: json: cannot unmarshal string into Go struct field streamInputMessage.message of type printmode.streamInputUserMessage"} diff --git a/plugins/antigravity-provider/test/fixtures/schema-user-message.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/schema-user-message.stdin.ndjson new file mode 100644 index 0000000000..cbedcbaceb --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/schema-user-message.stdin.ndjson @@ -0,0 +1 @@ +{"t_ms": 383, "stream": "stdin", "data": {"event": "user_message", "message": "Reply PHASE0_HELLO."}} diff --git a/plugins/antigravity-provider/test/fixtures/schema-user-message.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/schema-user-message.stdout.ndjson new file mode 100644 index 0000000000..333d3a9b8d --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/schema-user-message.stdout.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 383, "stream": "stdout", "data": {"event": "init", "conversation_id": "b70d4858-e4ed-469c-84d6-8fec25ceb1cb", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "request-review"}}} +{"t_ms": 386, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "b70d4858-e4ed-469c-84d6-8fec25ceb1cb", "status": "ERROR", "response": "", "error": "failed to decode stream input: json: cannot unmarshal string into Go struct field streamInputMessage.message of type printmode.streamInputUserMessage", "duration_seconds": 0, "num_turns": 0, "usage": {"input_tokens": 0, "output_tokens": 0, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 0}}}} diff --git a/plugins/antigravity-provider/test/fixtures/schema-user.capture.ndjson b/plugins/antigravity-provider/test/fixtures/schema-user.capture.ndjson new file mode 100644 index 0000000000..205f7c638b --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/schema-user.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 426, "stream": "capture", "data": {"exit_code": 1}} diff --git a/plugins/antigravity-provider/test/fixtures/schema-user.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/schema-user.stderr.ndjson new file mode 100644 index 0000000000..7ae241a87e --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/schema-user.stderr.ndjson @@ -0,0 +1 @@ +{"t_ms": 342, "stream": "stderr", "data": "error: failed to decode stream input: json: cannot unmarshal string into Go struct field streamInputMessage.message of type printmode.streamInputUserMessage"} diff --git a/plugins/antigravity-provider/test/fixtures/schema-user.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/schema-user.stdin.ndjson new file mode 100644 index 0000000000..5523b07666 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/schema-user.stdin.ndjson @@ -0,0 +1 @@ +{"t_ms": 338, "stream": "stdin", "data": {"event": "user", "message": "Reply PHASE0_HELLO."}} diff --git a/plugins/antigravity-provider/test/fixtures/schema-user.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/schema-user.stdout.ndjson new file mode 100644 index 0000000000..324e16471e --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/schema-user.stdout.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 338, "stream": "stdout", "data": {"event": "init", "conversation_id": "f57811d9-3f9d-42cc-870a-ffdaa85ba608", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "request-review"}}} +{"t_ms": 342, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "f57811d9-3f9d-42cc-870a-ffdaa85ba608", "status": "ERROR", "response": "", "error": "failed to decode stream input: json: cannot unmarshal string into Go struct field streamInputMessage.message of type printmode.streamInputUserMessage", "duration_seconds": 0, "num_turns": 0, "usage": {"input_tokens": 0, "output_tokens": 0, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 0}}}} diff --git a/plugins/antigravity-provider/test/fixtures/sigint-mid-text.capture.ndjson b/plugins/antigravity-provider/test/fixtures/sigint-mid-text.capture.ndjson new file mode 100644 index 0000000000..744e6a79b1 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/sigint-mid-text.capture.ndjson @@ -0,0 +1,3 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", "", "--disable-slash-commands", "--dangerously-skip-permissions"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 2000, "stream": "capture", "data": {"signal": "SIGINT", "trigger": {"conversation_id": "a2c2132f-0f10-400b-a142-74f82230f449", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "# The Architecture of In"}}} +{"t_ms": 2141, "stream": "capture", "data": {"exit_code": 1}} diff --git a/plugins/antigravity-provider/test/fixtures/sigint-mid-text.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/sigint-mid-text.stderr.ndjson new file mode 100644 index 0000000000..49b703e524 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/sigint-mid-text.stderr.ndjson @@ -0,0 +1 @@ +{"t_ms": 2025, "stream": "stderr", "data": "error: interrupted"} diff --git a/plugins/antigravity-provider/test/fixtures/sigint-mid-text.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/sigint-mid-text.stdin.ndjson new file mode 100644 index 0000000000..06c4053234 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/sigint-mid-text.stdin.ndjson @@ -0,0 +1 @@ +{"t_ms": 310, "stream": "stdin", "data": {"event": "user", "message": {"content": "Write a long 2000-word essay on prime numbers. No tools."}}} diff --git a/plugins/antigravity-provider/test/fixtures/sigint-mid-text.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/sigint-mid-text.stdout.ndjson new file mode 100644 index 0000000000..63b9bd96f1 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/sigint-mid-text.stdout.ndjson @@ -0,0 +1,4 @@ +{"t_ms": 309, "stream": "stdout", "data": {"event": "init", "conversation_id": "a2c2132f-0f10-400b-a142-74f82230f449", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "always-proceed"}}} +{"t_ms": 337, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a2c2132f-0f10-400b-a142-74f82230f449", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 2000, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "a2c2132f-0f10-400b-a142-74f82230f449", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "# The Architecture of In"}}} +{"t_ms": 2025, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "a2c2132f-0f10-400b-a142-74f82230f449", "status": "ERROR", "response": "# The Architecture of In", "error": "interrupted", "duration_seconds": 1.690084523, "num_turns": 1, "usage": {"input_tokens": 0, "output_tokens": 0, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 0}}}} diff --git a/plugins/antigravity-provider/test/fixtures/sigint-mid-tool.capture.ndjson b/plugins/antigravity-provider/test/fixtures/sigint-mid-tool.capture.ndjson new file mode 100644 index 0000000000..d4774ef9e5 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/sigint-mid-tool.capture.ndjson @@ -0,0 +1,3 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", "", "--dangerously-skip-permissions"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 4486, "stream": "capture", "data": {"signal": "SIGINT", "trigger": {"conversation_id": "b3c8b9d6-c0ca-4444-8fd3-8bc86de8e6ba", "step_index": 2, "state": "ACTIVE", "step_type": "tool", "tool_name": "run_command", "tool_info": {"name": "run_command", "parameters": {"CommandLine": "sleep 30; printf FINISHED"}}}}} +{"t_ms": 4716, "stream": "capture", "data": {"exit_code": 1}} diff --git a/plugins/antigravity-provider/test/fixtures/sigint-mid-tool.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/sigint-mid-tool.stderr.ndjson new file mode 100644 index 0000000000..40ea382edb --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/sigint-mid-tool.stderr.ndjson @@ -0,0 +1 @@ +{"t_ms": 4489, "stream": "stderr", "data": "error: interrupted"} diff --git a/plugins/antigravity-provider/test/fixtures/sigint-mid-tool.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/sigint-mid-tool.stdin.ndjson new file mode 100644 index 0000000000..7d34666611 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/sigint-mid-tool.stdin.ndjson @@ -0,0 +1 @@ +{"t_ms": 371, "stream": "stdin", "data": {"event": "user", "message": {"content": "Use run_command to run sleep 30; printf FINISHED in , WaitMsBeforeAsync 1000. Wait until it finishes."}}} diff --git a/plugins/antigravity-provider/test/fixtures/sigint-mid-tool.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/sigint-mid-tool.stdout.ndjson new file mode 100644 index 0000000000..ea8cee886f --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/sigint-mid-tool.stdout.ndjson @@ -0,0 +1,5 @@ +{"t_ms": 371, "stream": "stdout", "data": {"event": "init", "conversation_id": "b3c8b9d6-c0ca-4444-8fd3-8bc86de8e6ba", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "always-proceed"}}} +{"t_ms": 399, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b3c8b9d6-c0ca-4444-8fd3-8bc86de8e6ba", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 4086, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b3c8b9d6-c0ca-4444-8fd3-8bc86de8e6ba", "step_index": 1, "state": "DONE", "step_type": "agent_response", "duration_seconds": 3.660004339, "usage": {"input_tokens": 11272, "output_tokens": 247, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11519}}}} +{"t_ms": 4086, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "b3c8b9d6-c0ca-4444-8fd3-8bc86de8e6ba", "step_index": 2, "state": "ACTIVE", "step_type": "tool", "tool_name": "run_command", "tool_info": {"name": "run_command", "parameters": {"CommandLine": "sleep 30; printf FINISHED"}}}}} +{"t_ms": 4489, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "b3c8b9d6-c0ca-4444-8fd3-8bc86de8e6ba", "status": "ERROR", "response": "", "error": "interrupted", "duration_seconds": 4.114884661, "num_turns": 1, "usage": {"input_tokens": 11272, "output_tokens": 247, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11519}}}} diff --git a/plugins/antigravity-provider/test/fixtures/sigterm.capture.ndjson b/plugins/antigravity-provider/test/fixtures/sigterm.capture.ndjson new file mode 100644 index 0000000000..eafb4b76ed --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/sigterm.capture.ndjson @@ -0,0 +1,3 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", "", "--disable-slash-commands", "--dangerously-skip-permissions"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 316, "stream": "capture", "data": {"signal": "SIGTERM", "trigger": {"conversation_id": "21c82404-cd16-4781-a827-eb7186d61b7e", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 467, "stream": "capture", "data": {"exit_code": 1}} diff --git a/plugins/antigravity-provider/test/fixtures/sigterm.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/sigterm.stderr.ndjson new file mode 100644 index 0000000000..ee58454e54 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/sigterm.stderr.ndjson @@ -0,0 +1 @@ +{"t_ms": 341, "stream": "stderr", "data": "error: interrupted"} diff --git a/plugins/antigravity-provider/test/fixtures/sigterm.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/sigterm.stdin.ndjson new file mode 100644 index 0000000000..27e2a28a9c --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/sigterm.stdin.ndjson @@ -0,0 +1 @@ +{"t_ms": 289, "stream": "stdin", "data": {"event": "user", "message": {"content": "Write a long essay on prime numbers. No tools."}}} diff --git a/plugins/antigravity-provider/test/fixtures/sigterm.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/sigterm.stdout.ndjson new file mode 100644 index 0000000000..1fe0f471e1 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/sigterm.stdout.ndjson @@ -0,0 +1,3 @@ +{"t_ms": 289, "stream": "stdout", "data": {"event": "init", "conversation_id": "21c82404-cd16-4781-a827-eb7186d61b7e", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "always-proceed"}}} +{"t_ms": 316, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "21c82404-cd16-4781-a827-eb7186d61b7e", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 341, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "21c82404-cd16-4781-a827-eb7186d61b7e", "status": "ERROR", "response": "", "error": "interrupted", "duration_seconds": 0, "num_turns": 1, "usage": {"input_tokens": 0, "output_tokens": 0, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 0}}}} diff --git a/plugins/antigravity-provider/test/fixtures/slash-disabled.capture.ndjson b/plugins/antigravity-provider/test/fixtures/slash-disabled.capture.ndjson new file mode 100644 index 0000000000..72dc5418fc --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/slash-disabled.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--disable-slash-commands"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 9267, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/slash-disabled.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/slash-disabled.stderr.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/slash-disabled.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/slash-disabled.stdin.ndjson new file mode 100644 index 0000000000..fc08c01367 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/slash-disabled.stdin.ndjson @@ -0,0 +1 @@ +{"t_ms": 292, "stream": "stdin", "data": {"event": "user", "message": {"content": "/help"}}} diff --git a/plugins/antigravity-provider/test/fixtures/slash-disabled.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/slash-disabled.stdout.ndjson new file mode 100644 index 0000000000..5050830bb3 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/slash-disabled.stdout.ndjson @@ -0,0 +1,84 @@ +{"t_ms": 291, "stream": "stdout", "data": {"event": "init", "conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "request-review"}}} +{"t_ms": 318, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 3215, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 1, "state": "DONE", "step_type": "agent_response", "duration_seconds": 2.874473262, "usage": {"input_tokens": 11225, "output_tokens": 193, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11418}}}} +{"t_ms": 3215, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 2, "state": "ACTIVE", "step_type": "tool", "tool_name": "view_file", "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/.gemini/antigravity-cli/builtin/skills/antigravity_guide/SKILL.md"}}}}} +{"t_ms": 3215, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 2, "state": "DONE", "step_type": "tool", "tool_name": "view_file", "duration_seconds": 0.003097441, "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/.gemini/antigravity-cli/builtin/skills/antigravity_guide/SKILL.md"}, "output": "56 lines, 3023 bytes"}}}} +{"t_ms": 5737, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 3, "state": "DONE", "step_type": "agent_response", "duration_seconds": 2.513209521, "usage": {"input_tokens": 12487, "output_tokens": 73, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12560}}}} +{"t_ms": 5737, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 4, "state": "ACTIVE", "step_type": "tool", "tool_name": "view_file", "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/.gemini/antigravity-cli/builtin/skills/antigravity_guide/references/cli.md"}}}}} +{"t_ms": 5737, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 4, "state": "DONE", "step_type": "tool", "tool_name": "view_file", "duration_seconds": 0.003075841, "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/.gemini/antigravity-cli/builtin/skills/antigravity_guide/references/cli.md"}, "output": "42 lines, 1686 bytes"}}}} +{"t_ms": 6974, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "Here is"}}} +{"t_ms": 7051, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " a quick guide to using "}}} +{"t_ms": 7076, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "**Antigravit"}}} +{"t_ms": 7101, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "y (AGY)** and its availa"}}} +{"t_ms": 7127, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ble slash"}}} +{"t_ms": 7177, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " commands:\n\n### Available Slash Comm"}}} +{"t_ms": 7203, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ands\n\n* **/p"}}} +{"t_ms": 7228, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "lan** – Initiates deta"}}} +{"t_ms": 7254, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "iled step-by"}}} +{"t_ms": 7279, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "-step planning and archi"}}} +{"t_ms": 7305, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "tecture desi"}}} +{"t_ms": 7330, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "gn"}}} +{"t_ms": 7356, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " before making code changes.\n* **/gr"}}} +{"t_ms": 7381, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ill-me** –"}}} +{"t_ms": 7407, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " Starts an interactive i"}}} +{"t_ms": 7432, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "nterview/rev"}}} +{"t_ms": 7458, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "iew session to refine re"}}} +{"t_ms": 7483, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "quirements and"}}} +{"t_ms": 7535, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " architectural decisions"}}} +{"t_ms": 7560, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": ".\n* **/goal*"}}} +{"t_ms": 7586, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "* – Sets a high-level,"}}} +{"t_ms": 7611, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " long-running goal where"}}} +{"t_ms": 7637, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " the agent w"}}} +{"t_ms": 7662, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "orks autonomously and"}}} +{"t_ms": 7763, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " thoroughly "}}} +{"t_ms": 7789, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "until completion.\n* **/s"}}} +{"t_ms": 7814, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "chedule** "}}} +{"t_ms": 7840, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "– Sets up one-shot tim"}}} +{"t_ms": 7865, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ers or recur"}}} +{"t_ms": 7890, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ring cron jobs for periodic checks a"}}} +{"t_ms": 7916, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "nd background tasks.\n* *"}}} +{"t_ms": 7941, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "*/browser** "}}} +{"t_ms": 7966, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "– Directs browser auto"}}} +{"t_ms": 7992, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "mation, test"}}} +{"t_ms": 8017, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ing, and web research wo"}}} +{"t_ms": 8042, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "rkflows.\n* **/learn** "}}} +{"t_ms": 8068, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "– Teaches the agent pr"}}} +{"t_ms": 8094, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "eferences, project rules"}}} +{"t_ms": 8119, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": ", or recurri"}}} +{"t_ms": 8144, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ng setup procedures to p"}}} +{"t_ms": 8169, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ersist across future ses"}}} +{"t_ms": 8195, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "sions.\n\n---\n"}}} +{"t_ms": 8220, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "\n### What I Can Help You"}}} +{"t_ms": 8246, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " With\n\n*"}}} +{"t_ms": 8271, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " **Code Exploration & Ed"}}} +{"t_ms": 8296, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "iting**: Sea"}}} +{"t_ms": 8322, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "rch codebase, view/edit "}}} +{"t_ms": 8347, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "files, run tests, and de"}}} +{"t_ms": 8372, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "bug issues.\n"}}} +{"t_ms": 8398, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "* **Ref"}}} +{"t_ms": 8500, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "actoring & F"}}} +{"t_ms": 8525, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "eature Development**: Pl"}}} +{"t_ms": 8550, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "an, implemen"}}} +{"t_ms": 8576, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "t, and verify multi-file"}}} +{"t_ms": 8601, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " features.\n*"}}} +{"t_ms": 8626, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " **Automation & Subagent"}}} +{"t_ms": 8652, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "s**: Run concurrent task"}}} +{"t_ms": 8677, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "s"}}} +{"t_ms": 8703, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": ", terminal c"}}} +{"t_ms": 8728, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ommands, or "}}} +{"t_ms": 8754, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "delegate complex tasks t"}}} +{"t_ms": 8779, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "o subagents.\n* **Customi"}}} +{"t_ms": 8804, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "zations**: C"}}} +{"t_ms": 8830, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "onfigure [skills](file:/"}}} +{"t_ms": 8855, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "//slash-disab"}}} +{"t_ms": 8881, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "led-home/.gemini/antigra"}}} +{"t_ms": 8906, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "vity-cli/builtin/skills/"}}} +{"t_ms": 8931, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "agy-customiz"}}} +{"t_ms": 8957, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ations/SKILL.md), wor"}}} +{"t_ms": 8982, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "kspace rules"}}} +{"t_ms": 9007, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": ", hooks, and MCP servers"}}} +{"t_ms": 9033, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": ".\n\nLet me kn"}}} +{"t_ms": 9058, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ow what you would like t"}}} +{"t_ms": 9085, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "o work on!"}}} +{"t_ms": 9136, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "step_index": 5, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 3.416356468, "usage": {"input_tokens": 13315, "output_tokens": 309, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 13624}}}} +{"t_ms": 9162, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "84f9a66c-abf4-49f9-81bc-07fe9d6ef1a6", "status": "SUCCESS", "response": "Here is a quick guide to using **Antigravity (AGY)** and its available slash commands:\n\n### Available Slash Commands\n\n* **/plan** – Initiates detailed step-by-step planning and architecture design before making code changes.\n* **/grill-me** – Starts an interactive interview/review session to refine requirements and architectural decisions.\n* **/goal** – Sets a high-level, long-running goal where the agent works autonomously and thoroughly until completion.\n* **/schedule** – Sets up one-shot timers or recurring cron jobs for periodic checks and background tasks.\n* **/browser** – Directs browser automation, testing, and web research workflows.\n* **/learn** – Teaches the agent preferences, project rules, or recurring setup procedures to persist across future sessions.\n\n---\n\n### What I Can Help You With\n\n* **Code Exploration & Editing**: Search codebase, view/edit files, run tests, and debug issues.\n* **Refactoring & Feature Development**: Plan, implement, and verify multi-file features.\n* **Automation & Subagents**: Run concurrent tasks, terminal commands, or delegate complex tasks to subagents.\n* **Customizations**: Configure [skills](file:///.gemini/antigravity-cli/builtin/skills/agy-customizations/SKILL.md), workspace rules, hooks, and MCP servers.\n\nLet me know what you would like to work on!\n", "duration_seconds": 8.84160525, "num_turns": 1, "usage": {"input_tokens": 37027, "output_tokens": 575, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 37602}}}} diff --git a/plugins/antigravity-provider/test/fixtures/slash-enabled.capture.ndjson b/plugins/antigravity-provider/test/fixtures/slash-enabled.capture.ndjson new file mode 100644 index 0000000000..852d00e9cd --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/slash-enabled.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 483, "stream": "capture", "data": {"exit_code": 2}} diff --git a/plugins/antigravity-provider/test/fixtures/slash-enabled.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/slash-enabled.stderr.ndjson new file mode 100644 index 0000000000..cf7c668f40 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/slash-enabled.stderr.ndjson @@ -0,0 +1 @@ +{"t_ms": 375, "stream": "stderr", "data": "error: /help is answered by the CLI itself and is unavailable with --input-format stream-json; run it as its own --print /help invocation"} diff --git a/plugins/antigravity-provider/test/fixtures/slash-enabled.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/slash-enabled.stdin.ndjson new file mode 100644 index 0000000000..73da2a88d2 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/slash-enabled.stdin.ndjson @@ -0,0 +1 @@ +{"t_ms": 374, "stream": "stdin", "data": {"event": "user", "message": {"content": "/help"}}} diff --git a/plugins/antigravity-provider/test/fixtures/slash-enabled.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/slash-enabled.stdout.ndjson new file mode 100644 index 0000000000..072ae3c437 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/slash-enabled.stdout.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 374, "stream": "stdout", "data": {"event": "init", "conversation_id": "ca3eefcf-2a10-470a-ac64-81f29d305c40", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "request-review"}}} +{"t_ms": 375, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "ca3eefcf-2a10-470a-ac64-81f29d305c40", "status": "ERROR", "response": "", "error": "/help is answered by the CLI itself and is unavailable with --input-format stream-json; run it as its own --print /help invocation", "duration_seconds": 0, "num_turns": 0, "usage": {"input_tokens": 0, "output_tokens": 0, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 0}}}} diff --git a/plugins/antigravity-provider/test/fixtures/state-inspection.ndjson b/plugins/antigravity-provider/test/fixtures/state-inspection.ndjson new file mode 100644 index 0000000000..2155c5cd9f --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/state-inspection.ndjson @@ -0,0 +1,6 @@ +{"stream": "inspection", "data": {"command": "python3 /finalize.py", "sha256": "574b0234656c44564f3ff3b013225c86f14ae79b67d0f3b45f6aaf1b1625440e", "version_fixture": "version.stdout.ndjson:1", "operation": "hashlib.sha256( bytes)"}} +{"stream": "inspection", "data": {"command": "HOME= strace -f -e trace=file -o /files.trace /.local/opt/agy/1.2.13/agy --input-format stream-json --output-format stream-json", "accessed_state_paths": ["/.gemini/antigravity-cli/antigravity-oauth-token", "/.gemini/antigravity-cli/conversation_summaries.db", "/.gemini/antigravity-cli/conversations", "/.gemini/antigravity-cli/mcp_config.json", "/.gemini/antigravity-cli/secplugins/config.json", "/.gemini/antigravity-cli/settings.json", "/.gemini/antigravity/conversations", "/.gemini/config/config.json", "/.gemini/config/mcp_config.json"], "outside_isolated_home_gemini_accesses": []}} +{"stream": "inspection", "data": {"command": "python3 /finalize.py", "database": "/.gemini/antigravity-cli/conversation_summaries.db", "query": "SELECT conversation_id,title,preview,step_count,last_modified_time,workspace_uris,project_id,agent_name,parent_conversation_id,app_data_dir FROM conversation_summaries", "rows": [{"conversation_id": "b966b16a-caf4-448a-bb24-0e34956022b0", "title": "Secret Word Arithmetic Calculation", "preview": "Secret Word Arithmetic Calculation", "step_count": 13, "last_modified_time": "2026-09-29 18:42:36.36116556+00:00", "workspace_uris": "[\"file://\"]", "project_id": "default-cli-project", "agent_name": "", "parent_conversation_id": "", "app_data_dir": "antigravity-cli"}]}} +{"stream": "inspection", "data": {"database": "/.gemini/antigravity-cli/conversations/b966b16a-caf4-448a-bb24-0e34956022b0.db", "query": "SELECT name FROM sqlite_master WHERE type='table'", "tables": ["trajectory_meta", "steps", "gen_metadata", "executor_metadata", "parent_references", "trajectory_metadata_blob", "battle_mode_infos"], "steps_count": 13, "note": "Step payloads and metadata are binary blobs; no transcript decoder verified."}} +{"stream": "inspection", "data": {"command": "python3 /finalize.py", "workspace_files": {"sandbox.txt": "PHASE0_EDIT\n", "skip-permissions.txt": "PHASE0_EDIT\n", "plan.txt": "PHASE0_EDIT\n", "accept-edits.txt": "PHASE0_EDIT\n", "default.txt": "PHASE0_EDIT\n", "input.txt": "PHASE0_READ_MARKER\n"}}} +{"stream": "inspection", "data": {"command": "firecrawl scrape --format markdown", "retrieved_manifest": "```json\n{\n \"version\": \"1.2.12\",\n \"url\": \"https://storage.googleapis.com/antigravity-public/antigravity-cli/1.2.12-5784551402897408/linux-x64/cli_linux_x64.tar.gz\",\n \"sha512\": \"d5f0fe7433cb7c43ea878c07627a4fdb82d218f3bef5e6436266f5d9fdd2df145523453b9be0c4250391a64a007f5f42f7faff797bc2b2d502e7efb4874e383a\"\n}\n\n```\n", "note": "Fetch returned 1.2.12; supplied binary reports 1.2.13. Release provenance is not established by this manifest response."}} diff --git a/plugins/antigravity-provider/test/fixtures/stdin-close-mid-turn.capture.ndjson b/plugins/antigravity-provider/test/fixtures/stdin-close-mid-turn.capture.ndjson new file mode 100644 index 0000000000..be57e05c35 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/stdin-close-mid-turn.capture.ndjson @@ -0,0 +1,3 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", "", "--disable-slash-commands", "--dangerously-skip-permissions"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 319, "stream": "capture", "data": {"stdin_close": true}} +{"t_ms": 2576, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/stdin-close-mid-turn.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/stdin-close-mid-turn.stderr.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/stdin-close-mid-turn.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/stdin-close-mid-turn.stdin.ndjson new file mode 100644 index 0000000000..de27d4d5db --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/stdin-close-mid-turn.stdin.ndjson @@ -0,0 +1 @@ +{"t_ms": 292, "stream": "stdin", "data": {"event": "user", "message": {"content": "Reply with PHASE0_EOF. No tools."}}} diff --git a/plugins/antigravity-provider/test/fixtures/stdin-close-mid-turn.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/stdin-close-mid-turn.stdout.ndjson new file mode 100644 index 0000000000..88c41c8b1d --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/stdin-close-mid-turn.stdout.ndjson @@ -0,0 +1,5 @@ +{"t_ms": 292, "stream": "stdout", "data": {"event": "init", "conversation_id": "4a95b7fd-ae73-4b99-9c00-25d7c37f0250", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "always-proceed"}}} +{"t_ms": 319, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "4a95b7fd-ae73-4b99-9c00-25d7c37f0250", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 2230, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "4a95b7fd-ae73-4b99-9c00-25d7c37f0250", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "PHASE0_EOF"}}} +{"t_ms": 2459, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "4a95b7fd-ae73-4b99-9c00-25d7c37f0250", "step_index": 1, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 2.123780285, "usage": {"input_tokens": 11253, "output_tokens": 80, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11333}}}} +{"t_ms": 2459, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "4a95b7fd-ae73-4b99-9c00-25d7c37f0250", "status": "SUCCESS", "response": "PHASE0_EOF\n", "duration_seconds": 2.146186078, "num_turns": 1, "usage": {"input_tokens": 11253, "output_tokens": 80, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11333}}}} diff --git a/plugins/antigravity-provider/test/fixtures/stdin-mid-turn.capture.ndjson b/plugins/antigravity-provider/test/fixtures/stdin-mid-turn.capture.ndjson new file mode 100644 index 0000000000..12fb5138b4 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/stdin-mid-turn.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", "", "--disable-slash-commands", "--dangerously-skip-permissions"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 9564, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/stdin-mid-turn.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/stdin-mid-turn.stderr.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/stdin-mid-turn.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/stdin-mid-turn.stdin.ndjson new file mode 100644 index 0000000000..326c02c7d0 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/stdin-mid-turn.stdin.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 971, "stream": "stdin", "data": {"event": "user", "message": {"content": "Write a 500-word essay about primes. No tools."}}} +{"t_ms": 997, "stream": "stdin", "data": {"event": "user", "message": {"content": "Reply with exactly PHASE0_QUEUED. No tools."}}} diff --git a/plugins/antigravity-provider/test/fixtures/stdin-mid-turn.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/stdin-mid-turn.stdout.ndjson new file mode 100644 index 0000000000..bf80956e6a --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/stdin-mid-turn.stdout.ndjson @@ -0,0 +1,179 @@ +{"t_ms": 971, "stream": "stdout", "data": {"event": "init", "conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "always-proceed"}}} +{"t_ms": 997, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 3035, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "### The Indivisible Atom"}}} +{"t_ms": 3061, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "s of Arithme"}}} +{"t_ms": 3087, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "tic\n\nIn the vast landsca"}}} +{"t_ms": 3112, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "pe of mathem"}}} +{"t_ms": 3137, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "atics, prime numbers sta"}}} +{"t_ms": 3162, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "nd as"}}} +{"t_ms": 3213, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " fundamental building bl"}}} +{"t_ms": 3238, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ocks—the i"}}} +{"t_ms": 3263, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ndivisible atoms of arit"}}} +{"t_ms": 3289, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "hmetic. Defi"}}} +{"t_ms": 3314, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ned simply as integers g"}}} +{"t_ms": 3339, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "reater than "}}} +{"t_ms": 3365, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "one that are divisible o"}}} +{"t_ms": 3390, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "nly by one"}}} +{"t_ms": 3441, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " and themselves, primes possess an e"}}} +{"t_ms": 3466, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "lemental pur"}}} +{"t_ms": 3491, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ity. Yet beneath this de"}}} +{"t_ms": 3517, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ceptive simplicity lies "}}} +{"t_ms": 3542, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "one of the d"}}} +{"t_ms": 3567, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "eepest, most beautiful m"}}} +{"t_ms": 3592, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ysteries in "}}} +{"t_ms": 3618, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "human"}}} +{"t_ms": 3668, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " inquiry: an"}}} +{"t_ms": 3694, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " unending dance between "}}} +{"t_ms": 3719, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "order and ch"}}} +{"t_ms": 3744, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "aos, predictability and "}}} +{"t_ms": 3770, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "randomness.\n"}}} +{"t_ms": 3795, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "\nThe fascination with pr"}}} +{"t_ms": 3820, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "imes stretches back to antiquity. In"}}} +{"t_ms": 3846, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " approximate"}}} +{"t_ms": 3871, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ly 300 BCE, the ancient "}}} +{"t_ms": 3897, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "Greek mathem"}}} +{"t_ms": 3922, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "atician Euclid formulate"}}} +{"t_ms": 3947, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "d one of history’s mos"}}} +{"t_ms": 3972, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "t elegant proofs in his "}}} +{"t_ms": 3998, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "*Elements*, demonstratin"}}} +{"t_ms": 4023, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "g that the s"}}} +{"t_ms": 4048, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "upply of prime numbers i"}}} +{"t_ms": 4074, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "s infinite. "}}} +{"t_ms": 4099, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "If one assumes a finite "}}} +{"t_ms": 4124, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "list of primes, multiply"}}} +{"t_ms": 4150, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ing them tog"}}} +{"t_ms": 4175, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ether and adding one yields a new nu"}}} +{"t_ms": 4201, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "mber that is"}}} +{"t_ms": 4226, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " either prime itself or "}}} +{"t_ms": 4251, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "divisible by"}}} +{"t_ms": 4277, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " a prime not on the orig"}}} +{"t_ms": 4303, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "inal list. Euclid’s pr"}}} +{"t_ms": 4328, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "oof establis"}}} +{"t_ms": 4354, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "hed an eternal truth: no matter how "}}} +{"t_ms": 4379, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "far we trave"}}} +{"t_ms": 4404, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "l along the number line,"}}} +{"t_ms": 4430, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " new primes will continu"}}} +{"t_ms": 4455, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "e to emerge."}}} +{"t_ms": 4481, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "\n\nDespite their infinite"}}} +{"t_ms": 4583, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " nature, primes become i"}}} +{"t_ms": 4608, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ncreasingly sparse as nu"}}} +{"t_ms": 4633, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "mbers grow l"}}} +{"t_ms": 4659, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "arger. For centuries, th"}}} +{"t_ms": 4684, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "eir distribu"}}} +{"t_ms": 4709, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "tion appeared entirely e"}}} +{"t_ms": 4734, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "rratic, defying simple formulas. It "}}} +{"t_ms": 4760, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "was not unti"}}} +{"t_ms": 4785, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "l the nineteenth century"}}} +{"t_ms": 4810, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " that Carl F"}}} +{"t_ms": 4836, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "riedrich Gauss and Adrie"}}} +{"t_ms": 4861, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "n-Marie Legendre conject"}}} +{"t_ms": 4886, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ured the Pri"}}} +{"t_ms": 4912, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "me Number Theorem, later"}}} +{"t_ms": 4937, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " proven in 1896 by Jacqu"}}} +{"t_ms": 4963, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "es Hadamard and Charles "}}} +{"t_ms": 4988, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "Jean de la V"}}} +{"t_ms": 5013, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "allée Poussin. The theo"}}} +{"t_ms": 5038, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "rem revealed a breathtak"}}} +{"t_ms": 5064, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ing macrosco"}}} +{"t_ms": 5089, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "pic regularity: the probabi"}}} +{"t_ms": 5114, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "lity that a "}}} +{"t_ms": 5140, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "randomly chosen integer "}}} +{"t_ms": 5165, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "$N$ is prime is inversel"}}} +{"t_ms": 5190, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "y proportion"}}} +{"t_ms": 5216, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "al to its natural logari"}}} +{"t_ms": 5241, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "thm, $\\ln"}}} +{"t_ms": 5292, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "(N)$. While individual p"}}} +{"t_ms": 5317, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "rimes appear"}}} +{"t_ms": 5343, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " almost randomly, their "}}} +{"t_ms": 5368, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "collective b"}}} +{"t_ms": 5393, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ehavior follows a remark"}}} +{"t_ms": 5418, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ably smooth "}}} +{"t_ms": 5444, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "logarithmic curve.\n\nThis"}}} +{"t_ms": 5469, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " duality between"}}} +{"t_ms": 5495, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " local chaos and global "}}} +{"t_ms": 5520, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "harmony is epitomized by"}}} +{"t_ms": 5546, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " the Riemann"}}} +{"t_ms": 5571, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " Hypothesis, proposed by"}}} +{"t_ms": 5596, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " Bernhard Riemann in 185"}}} +{"t_ms": 5622, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "9. By"}}} +{"t_ms": 5673, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " extending t"}}} +{"t_ms": 5698, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "he zeta function into th"}}} +{"t_ms": 5723, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "e complex pl"}}} +{"t_ms": 5749, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ane, Riemann connected t"}}} +{"t_ms": 5774, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "he distribution of prime"}}} +{"t_ms": 5799, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "s directly t"}}} +{"t_ms": 5825, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "o the non-trivial zeros "}}} +{"t_ms": 5850, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "of this func"}}} +{"t_ms": 5875, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "tion. Widely regarded as the most"}}} +{"t_ms": 5900, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " famous unsolved problem"}}} +{"t_ms": 5926, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " in mathemat"}}} +{"t_ms": 5952, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ics, the Riemann Hypothe"}}} +{"t_ms": 5977, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "sis asserts "}}} +{"t_ms": 6002, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "that all these non-trivi"}}} +{"t_ms": 6028, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "al zeros lie"}}} +{"t_ms": 6053, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " on a"}}} +{"t_ms": 6104, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " single critical line. I"}}} +{"t_ms": 6129, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "f true, it would unlock "}}} +{"t_ms": 6154, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "a profound h"}}} +{"t_ms": 6180, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "armonic symphony governi"}}} +{"t_ms": 6205, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ng the exact fluctuation"}}} +{"t_ms": 6230, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "s of prime n"}}} +{"t_ms": 6256, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "umbers.\n\nBeyond"}}} +{"t_ms": 6332, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " their aesthetic allure in pure math"}}} +{"t_ms": 6357, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ematics, pri"}}} +{"t_ms": 6383, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "me numbers have quietly "}}} +{"t_ms": 6408, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "become the b"}}} +{"t_ms": 6434, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "edrock of modern technological civil"}}} +{"t_ms": 6459, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ization. In "}}} +{"t_ms": 6485, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "the 1970s, the developme"}}} +{"t_ms": 6510, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "nt of asymmetric public-"}}} +{"t_ms": 6535, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "key cryptogr"}}} +{"t_ms": 6561, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "aphy—most notably the "}}} +{"t_ms": 6586, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "RSA algorithm—transfor"}}} +{"t_ms": 6611, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "med prime numbers into i"}}} +{"t_ms": 6637, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ndispensable digital gua"}}} +{"t_ms": 6662, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "rdians. The "}}} +{"t_ms": 6687, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "security of global comme"}}} +{"t_ms": 6713, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "rce, secure "}}} +{"t_ms": 6738, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "communication, and onlin"}}} +{"t_ms": 6763, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "e privacy re"}}} +{"t_ms": 6789, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "sts upon a computational asymmetry: "}}} +{"t_ms": 6814, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "multiplying two large pr"}}} +{"t_ms": 6839, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ime numbers "}}} +{"t_ms": 6865, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "together is nearly insta"}}} +{"t_ms": 6890, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ntaneous, bu"}}} +{"t_ms": 6915, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "t factoring their astron"}}} +{"t_ms": 6941, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "omical product back into"}}} +{"t_ms": 6966, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " its constit"}}} +{"t_ms": 6991, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "uent primes is virtually"}}} +{"t_ms": 7017, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " impossible "}}} +{"t_ms": 7042, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "with classical computing resources.\n"}}} +{"t_ms": 7067, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "\nPrime numbe"}}} +{"t_ms": 7093, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "rs represent a sublime i"}}} +{"t_ms": 7118, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ntersection of philosoph"}}} +{"t_ms": 7143, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "y, abstract "}}} +{"t_ms": 7169, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "theory, and practical ut"}}} +{"t_ms": 7194, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ility. They "}}} +{"t_ms": 7219, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "remind us that the most "}}} +{"t_ms": 7245, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "basic structures in natu"}}} +{"t_ms": 7270, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "re often harbor the deep"}}} +{"t_ms": 7295, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "est questions. As mathem"}}} +{"t_ms": 7320, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "aticians con"}}} +{"t_ms": 7346, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "tinue to explore unresol"}}} +{"t_ms": 7371, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ved enigmas "}}} +{"t_ms": 7397, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "like the Twin Prime Conjecture or th"}}} +{"t_ms": 7422, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "e Goldbach Conjecture, p"}}} +{"t_ms": 7448, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "rimes remain"}}} +{"t_ms": 7473, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " an enduring testament t"}}} +{"t_ms": 7498, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "o the infini"}}} +{"t_ms": 7524, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "te richness waiting to b"}}} +{"t_ms": 7549, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "e uncovered within the s"}}} +{"t_ms": 7574, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "implest conc"}}} +{"t_ms": 7600, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "epts."}}} +{"t_ms": 7650, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 1, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 6.647623906, "usage": {"input_tokens": 11252, "output_tokens": 639, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11891}}}} +{"t_ms": 7650, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "status": "SUCCESS", "response": "### The Indivisible Atoms of Arithmetic\n\nIn the vast landscape of mathematics, prime numbers stand as fundamental building blocks—the indivisible atoms of arithmetic. Defined simply as integers greater than one that are divisible only by one and themselves, primes possess an elemental purity. Yet beneath this deceptive simplicity lies one of the deepest, most beautiful mysteries in human inquiry: an unending dance between order and chaos, predictability and randomness.\n\nThe fascination with primes stretches back to antiquity. In approximately 300 BCE, the ancient Greek mathematician Euclid formulated one of history’s most elegant proofs in his *Elements*, demonstrating that the supply of prime numbers is infinite. If one assumes a finite list of primes, multiplying them together and adding one yields a new number that is either prime itself or divisible by a prime not on the original list. Euclid’s proof established an eternal truth: no matter how far we travel along the number line, new primes will continue to emerge.\n\nDespite their infinite nature, primes become increasingly sparse as numbers grow larger. For centuries, their distribution appeared entirely erratic, defying simple formulas. It was not until the nineteenth century that Carl Friedrich Gauss and Adrien-Marie Legendre conjectured the Prime Number Theorem, later proven in 1896 by Jacques Hadamard and Charles Jean de la Vallée Poussin. The theorem revealed a breathtaking macroscopic regularity: the probability that a randomly chosen integer $N$ is prime is inversely proportional to its natural logarithm, $\\ln(N)$. While individual primes appear almost randomly, their collective behavior follows a remarkably smooth logarithmic curve.\n\nThis duality between local chaos and global harmony is epitomized by the Riemann Hypothesis, proposed by Bernhard Riemann in 1859. By extending the zeta function into the complex plane, Riemann connected the distribution of primes directly to the non-trivial zeros of this function. Widely regarded as the most famous unsolved problem in mathematics, the Riemann Hypothesis asserts that all these non-trivial zeros lie on a single critical line. If true, it would unlock a profound harmonic symphony governing the exact fluctuations of prime numbers.\n\nBeyond their aesthetic allure in pure mathematics, prime numbers have quietly become the bedrock of modern technological civilization. In the 1970s, the development of asymmetric public-key cryptography—most notably the RSA algorithm—transformed prime numbers into indispensable digital guardians. The security of global commerce, secure communication, and online privacy rests upon a computational asymmetry: multiplying two large prime numbers together is nearly instantaneous, but factoring their astronomical product back into its constituent primes is virtually impossible with classical computing resources.\n\nPrime numbers represent a sublime intersection of philosophy, abstract theory, and practical utility. They remind us that the most basic structures in nature often harbor the deepest questions. As mathematicians continue to explore unresolved enigmas like the Twin Prime Conjecture or the Goldbach Conjecture, primes remain an enduring testament to the infinite richness waiting to be uncovered within the simplest concepts.\n", "duration_seconds": 6.674385393, "num_turns": 1, "usage": {"input_tokens": 11252, "output_tokens": 639, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11891}}}} +{"t_ms": 7677, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 2, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 9187, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "PHASE"}}} +{"t_ms": 9213, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "0_QUEUED"}}} +{"t_ms": 9466, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "step_index": 3, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 1.793731176, "usage": {"input_tokens": 11913, "output_tokens": 6, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11919}}}} +{"t_ms": 9466, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "956514b0-986c-4168-8130-c6251328d6fb", "status": "SUCCESS", "response": "PHASE0_QUEUED\n", "duration_seconds": 8.483804893, "num_turns": 2, "usage": {"input_tokens": 23165, "output_tokens": 645, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 23810}}}} diff --git a/plugins/antigravity-provider/test/fixtures/subagent.capture.ndjson b/plugins/antigravity-provider/test/fixtures/subagent.capture.ndjson new file mode 100644 index 0000000000..854c3c3eba --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/subagent.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", "", "--dangerously-skip-permissions"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 13163, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/subagent.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/subagent.stderr.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/subagent.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/subagent.stdin.ndjson new file mode 100644 index 0000000000..0814f42805 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/subagent.stdin.ndjson @@ -0,0 +1 @@ +{"t_ms": 291, "stream": "stdin", "data": {"event": "user", "message": {"content": "Use define_subagent to define a small subagent named phase0-counter that answers arithmetic only, then invoke_subagent to ask it 2+2. Report its answer. Do not use shell."}}} diff --git a/plugins/antigravity-provider/test/fixtures/subagent.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/subagent.stdout.ndjson new file mode 100644 index 0000000000..cec633cda7 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/subagent.stdout.ndjson @@ -0,0 +1,23 @@ +{"t_ms": 291, "stream": "stdout", "data": {"event": "init", "conversation_id": "eceffa7f-bbef-47d0-a3cf-3daeaf20a829", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "always-proceed"}}} +{"t_ms": 319, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "eceffa7f-bbef-47d0-a3cf-3daeaf20a829", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 3924, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "eceffa7f-bbef-47d0-a3cf-3daeaf20a829", "step_index": 1, "state": "DONE", "step_type": "agent_response", "duration_seconds": 3.580501478, "usage": {"input_tokens": 11252, "output_tokens": 188, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11440}}}} +{"t_ms": 3924, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "eceffa7f-bbef-47d0-a3cf-3daeaf20a829", "step_index": 2, "state": "ACTIVE", "step_type": "tool", "tool_name": "define_subagent", "tool_info": {"name": "define_subagent", "parameters": {"description": "A subagent specialized in answering arithmetic questions only.", "name": "phase0-counter"}}}}} +{"t_ms": 3924, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "eceffa7f-bbef-47d0-a3cf-3daeaf20a829", "step_index": 2, "state": "DONE", "step_type": "tool", "tool_name": "define_subagent", "duration_seconds": 0.003321972, "tool_info": {"name": "define_subagent", "parameters": {"description": "A subagent specialized in answering arithmetic questions only.", "name": "phase0-counter"}, "output": "Subagent \"phase0-counter\" defined successfully. It can now be invoked via invoke_subagent."}}}} +{"t_ms": 6541, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "eceffa7f-bbef-47d0-a3cf-3daeaf20a829", "step_index": 3, "state": "DONE", "step_type": "agent_response", "duration_seconds": 2.634297625, "usage": {"input_tokens": 11433, "output_tokens": 79, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11512}}}} +{"t_ms": 6541, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "eceffa7f-bbef-47d0-a3cf-3daeaf20a829", "step_index": 4, "state": "ACTIVE", "step_type": "subagent", "tool_name": "invoke_subagent", "subagent_info": {"subagents": [{"type_name": "phase0-counter", "role": "Arithmetic Assistant", "initial_prompt": "What is 2+2?"}]}}}} +{"t_ms": 6565, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "eceffa7f-bbef-47d0-a3cf-3daeaf20a829", "step_index": 4, "state": "DONE", "step_type": "subagent", "tool_name": "invoke_subagent", "duration_seconds": 0.01034356, "subagent_info": {"subagents": [{"type_name": "phase0-counter", "role": "Arithmetic Assistant", "initial_prompt": "What is 2+2?", "conversation_id": "b6f02329-7415-49a7-a9a9-1cd09e898d26", "log_uri": "file:///.gemini/antigravity-cli/brain/b6f02329-7415-49a7-a9a9-1cd09e898d26/.system_generated/logs/transcript.jsonl", "workspace_uris": ["file://"]}]}}}} +{"t_ms": 8527, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "eceffa7f-bbef-47d0-a3cf-3daeaf20a829", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "I"}}} +{"t_ms": 8679, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "eceffa7f-bbef-47d0-a3cf-3daeaf20a829", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " have define"}}} +{"t_ms": 8704, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "eceffa7f-bbef-47d0-a3cf-3daeaf20a829", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "d the subagent `phase0-c"}}} +{"t_ms": 8730, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "eceffa7f-bbef-47d0-a3cf-3daeaf20a829", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ounter` and "}}} +{"t_ms": 8755, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "eceffa7f-bbef-47d0-a3cf-3daeaf20a829", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "invoked it with the ques"}}} +{"t_ms": 8780, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "eceffa7f-bbef-47d0-a3cf-3daeaf20a829", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "tion \"What"}}} +{"t_ms": 8806, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "eceffa7f-bbef-47d0-a3cf-3daeaf20a829", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " is 2+2?\". Awaiting its "}}} +{"t_ms": 8831, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "eceffa7f-bbef-47d0-a3cf-3daeaf20a829", "step_index": 5, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "response."}}} +{"t_ms": 9135, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "eceffa7f-bbef-47d0-a3cf-3daeaf20a829", "step_index": 5, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 2.58161986, "usage": {"input_tokens": 11739, "output_tokens": 31, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11770}}}} +{"t_ms": 9715, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "eceffa7f-bbef-47d0-a3cf-3daeaf20a829", "step_index": 6, "state": "DONE", "step_type": "system_message", "duration_seconds": 3.6749e-05}}} +{"t_ms": 12484, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "eceffa7f-bbef-47d0-a3cf-3daeaf20a829", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "The `phase0-"}}} +{"t_ms": 12510, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "eceffa7f-bbef-47d0-a3cf-3daeaf20a829", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "counter` sub"}}} +{"t_ms": 12610, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "eceffa7f-bbef-47d0-a3cf-3daeaf20a829", "step_index": 7, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "agent answered: **4**."}}} +{"t_ms": 13042, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "eceffa7f-bbef-47d0-a3cf-3daeaf20a829", "step_index": 7, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 3.329676969, "usage": {"input_tokens": 11886, "output_tokens": 74, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11960}}}} +{"t_ms": 13068, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "eceffa7f-bbef-47d0-a3cf-3daeaf20a829", "status": "SUCCESS", "response": "I have defined the subagent `phase0-counter` and invoked it with the question \"What is 2+2?\". Awaiting its response.\nThe `phase0-counter` subagent answered: **4**.\n", "duration_seconds": 12.746450429, "num_turns": 1, "usage": {"input_tokens": 46310, "output_tokens": 372, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 46682}}}} diff --git a/plugins/antigravity-provider/test/fixtures/text-block.capture.ndjson b/plugins/antigravity-provider/test/fixtures/text-block.capture.ndjson new file mode 100644 index 0000000000..b1f7ab6455 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/text-block.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", ""], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 4102, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/text-block.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/text-block.stderr.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/text-block.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/text-block.stdin.ndjson new file mode 100644 index 0000000000..faeb98b247 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/text-block.stdin.ndjson @@ -0,0 +1 @@ +{"t_ms": 2020, "stream": "stdin", "data": {"event": "user", "message": {"content": [{"type": "text", "text": "Reply with PHASE0_BLOCK. No tools."}]}}} diff --git a/plugins/antigravity-provider/test/fixtures/text-block.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/text-block.stdout.ndjson new file mode 100644 index 0000000000..ee7a7ec32a --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/text-block.stdout.ndjson @@ -0,0 +1,5 @@ +{"t_ms": 2020, "stream": "stdout", "data": {"event": "init", "conversation_id": "0757bec3-d263-4f90-bf34-7510d638d8c5", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "request-review"}}} +{"t_ms": 2051, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0757bec3-d263-4f90-bf34-7510d638d8c5", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 3939, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0757bec3-d263-4f90-bf34-7510d638d8c5", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "PHASE0_BLOCK"}}} +{"t_ms": 3990, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "0757bec3-d263-4f90-bf34-7510d638d8c5", "step_index": 1, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 1.932265932, "usage": {"input_tokens": 11235, "output_tokens": 62, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11297}}}} +{"t_ms": 3990, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "0757bec3-d263-4f90-bf34-7510d638d8c5", "status": "SUCCESS", "response": "PHASE0_BLOCK\n", "duration_seconds": 1.959296182, "num_turns": 1, "usage": {"input_tokens": 11235, "output_tokens": 62, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11297}}}} diff --git a/plugins/antigravity-provider/test/fixtures/text-turn.capture.ndjson b/plugins/antigravity-provider/test/fixtures/text-turn.capture.ndjson new file mode 100644 index 0000000000..1b006f8ab9 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/text-turn.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", ""], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 2911, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/text-turn.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/text-turn.stderr.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/text-turn.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/text-turn.stdin.ndjson new file mode 100644 index 0000000000..389b97dece --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/text-turn.stdin.ndjson @@ -0,0 +1 @@ +{"t_ms": 294, "stream": "stdin", "data": {"event": "user", "message": {"role": "user", "content": "Reply with exactly PHASE0_HELLO. No tools."}}} diff --git a/plugins/antigravity-provider/test/fixtures/text-turn.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/text-turn.stdout.ndjson new file mode 100644 index 0000000000..89fc8a7677 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/text-turn.stdout.ndjson @@ -0,0 +1,5 @@ +{"t_ms": 294, "stream": "stdout", "data": {"event": "init", "conversation_id": "e5cf1e2d-c715-4328-8e67-4a85c8dc3cda", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "request-review"}}} +{"t_ms": 351, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e5cf1e2d-c715-4328-8e67-4a85c8dc3cda", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 2544, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e5cf1e2d-c715-4328-8e67-4a85c8dc3cda", "step_index": 1, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "PHASE0_HELLO"}}} +{"t_ms": 2746, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "e5cf1e2d-c715-4328-8e67-4a85c8dc3cda", "step_index": 1, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 2.413995705, "usage": {"input_tokens": 11235, "output_tokens": 78, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11313}}}} +{"t_ms": 2772, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "e5cf1e2d-c715-4328-8e67-4a85c8dc3cda", "status": "SUCCESS", "response": "PHASE0_HELLO\n", "duration_seconds": 2.445370438, "num_turns": 1, "usage": {"input_tokens": 11235, "output_tokens": 78, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11313}}}} diff --git a/plugins/antigravity-provider/test/fixtures/tools-accept-edits.capture.ndjson b/plugins/antigravity-provider/test/fixtures/tools-accept-edits.capture.ndjson new file mode 100644 index 0000000000..d7c50c17a4 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/tools-accept-edits.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", "", "--disable-slash-commands", "--mode", "accept-edits"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 16127, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/tools-accept-edits.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/tools-accept-edits.stderr.ndjson new file mode 100644 index 0000000000..30cf90e622 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/tools-accept-edits.stderr.ndjson @@ -0,0 +1 @@ +{"t_ms": 15954, "stream": "stderr", "data": "jetski: no output produced — a tool required the \"command\" permission that headless mode cannot prompt for, so it was auto-denied. Add an allow-rule under permissions.allow in settings.json (e.g. command()). Alternatively, re-run with --dangerously-skip-permissions to auto-approve all tools."} diff --git a/plugins/antigravity-provider/test/fixtures/tools-accept-edits.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/tools-accept-edits.stdin.ndjson new file mode 100644 index 0000000000..4d89737919 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/tools-accept-edits.stdin.ndjson @@ -0,0 +1,3 @@ +{"t_ms": 288, "stream": "stdin", "data": {"event": "user", "message": {"role": "user", "content": "Use view_file to read /input.txt. Report its contents. Do not use a shell."}}} +{"t_ms": 5150, "stream": "stdin", "data": {"event": "user", "message": {"role": "user", "content": "Use write_to_file to create /accept-edits.txt with PHASE0_WRITE. Then use replace_file_content to change PHASE0_WRITE to PHASE0_EDIT. No shell. Do not ask me questions."}}} +{"t_ms": 13611, "stream": "stdin", "data": {"event": "user", "message": {"role": "user", "content": "Use run_command exactly once to run printf PHASE0_SHELL in . Report the output. Do not ask me questions."}}} diff --git a/plugins/antigravity-provider/test/fixtures/tools-accept-edits.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/tools-accept-edits.stdout.ndjson new file mode 100644 index 0000000000..57d50ad776 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/tools-accept-edits.stdout.ndjson @@ -0,0 +1,40 @@ +{"t_ms": 288, "stream": "stdout", "data": {"event": "init", "conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "request-review"}}} +{"t_ms": 315, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 2807, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 1, "state": "DONE", "step_type": "agent_response", "duration_seconds": 2.468344855, "usage": {"input_tokens": 11272, "output_tokens": 149, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11421}}}} +{"t_ms": 2807, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 2, "state": "ACTIVE", "step_type": "tool", "tool_name": "view_file", "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/input.txt"}}}}} +{"t_ms": 2807, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 2, "state": "DONE", "step_type": "tool", "tool_name": "view_file", "duration_seconds": 0.002720597, "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/input.txt"}, "output": "2 lines, 19 bytes"}}}} +{"t_ms": 4820, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "The contents"}}} +{"t_ms": 4870, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " of [/workspa"}}} +{"t_ms": 4896, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ce/input.txt"}}} +{"t_ms": 4921, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "](file:///tmp"}}} +{"t_ms": 4972, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "/agy-p0/work"}}} +{"t_ms": 4998, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "space/input.txt) are:\n\n```\nPHASE0_RE"}}} +{"t_ms": 5023, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "AD_MARKER\n``"}}} +{"t_ms": 5048, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "`"}}} +{"t_ms": 5150, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 3, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 2.360276704, "usage": {"input_tokens": 11494, "output_tokens": 47, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11541}}}} +{"t_ms": 5150, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "status": "SUCCESS", "response": "The contents of [/input.txt](file:///input.txt) are:\n\n```\nPHASE0_READ_MARKER\n```\n", "duration_seconds": 4.857244426, "num_turns": 1, "usage": {"input_tokens": 22766, "output_tokens": 196, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 22962}}}} +{"t_ms": 5176, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 4, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 8771, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 5, "state": "DONE", "step_type": "agent_response", "duration_seconds": 3.600197654, "usage": {"input_tokens": 11663, "output_tokens": 419, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12082}}}} +{"t_ms": 8771, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 6, "state": "ACTIVE", "step_type": "tool", "tool_name": "write_to_file", "tool_info": {"name": "write_to_file", "parameters": {"TargetFile": "/accept-edits.txt"}}}}} +{"t_ms": 8771, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 6, "state": "DONE", "step_type": "tool", "tool_name": "write_to_file", "duration_seconds": 0.003051666, "tool_info": {"name": "write_to_file", "parameters": {"TargetFile": "/accept-edits.txt"}}}}} +{"t_ms": 11213, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 7, "state": "DONE", "step_type": "agent_response", "duration_seconds": 2.42873306, "usage": {"input_tokens": 11846, "output_tokens": 129, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11975}}}} +{"t_ms": 11213, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 8, "state": "ACTIVE", "step_type": "tool", "tool_name": "replace_file_content", "tool_info": {"name": "replace_file_content", "parameters": {"TargetFile": "/accept-edits.txt"}}}}} +{"t_ms": 11213, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 8, "state": "DONE", "step_type": "tool", "tool_name": "replace_file_content", "duration_seconds": 0.003025767, "tool_info": {"name": "replace_file_content", "parameters": {"TargetFile": "/accept-edits.txt"}}}}} +{"t_ms": 12799, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "The file"}}} +{"t_ms": 12900, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " [/"}}} +{"t_ms": 12926, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "accept-edits"}}} +{"t_ms": 12951, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": ".txt](file:///"}}} +{"t_ms": 13002, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "tmp/agy-p0/w"}}} +{"t_ms": 13027, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "orkspace/accept-edits.tx"}}} +{"t_ms": 13053, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "t) has been "}}} +{"t_ms": 13078, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "created with `PHASE0"}}} +{"t_ms": 13155, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "_WRITE` and then updated"}}} +{"t_ms": 13180, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " to `PHASE0_"}}} +{"t_ms": 13206, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "EDIT`."}}} +{"t_ms": 13611, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 9, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 2.385314949, "usage": {"input_tokens": 12152, "output_tokens": 59, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12211}}}} +{"t_ms": 13611, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "status": "SUCCESS", "response": "The file [/accept-edits.txt](file:///accept-edits.txt) has been created with `PHASE0_WRITE` and then updated to `PHASE0_EDIT`.\n", "duration_seconds": 13.300599177, "num_turns": 2, "usage": {"input_tokens": 58427, "output_tokens": 803, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 59230}}}} +{"t_ms": 13638, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 10, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 15928, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 11, "state": "DONE", "step_type": "agent_response", "duration_seconds": 2.302666458, "usage": {"input_tokens": 12310, "output_tokens": 98, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12408}}}} +{"t_ms": 15928, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 12, "state": "ACTIVE", "step_type": "tool", "tool_name": "run_command", "tool_info": {"name": "run_command", "parameters": {"CommandLine": "printf PHASE0_SHELL"}}}}} +{"t_ms": 15954, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "step_index": 12, "state": "ERROR", "step_type": "tool", "tool_name": "run_command", "duration_seconds": 0.002216645, "tool_info": {"name": "run_command", "parameters": {"CommandLine": "printf PHASE0_SHELL"}, "error": {"type": "TOOL_ERROR", "message": "permission check failed for command \"printf PHASE0_SHELL\": user denied permission to run command:\nprintf PHASE0_SHELL\nDo not attempt to circumvent this denial by rephrasing the command, using alternative tools/scripts (e.g. python, sh, curl), or accessing the same target resource. Proceed without performing this action."}}}}} +{"t_ms": 15954, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "c38f8e92-993c-453c-9774-035e82a1108b", "status": "SUCCESS", "response": "", "duration_seconds": 15.639601905, "num_turns": 3, "usage": {"input_tokens": 70737, "output_tokens": 901, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 71638}, "denied_actions": [{"action": "command", "display_name": "RunCommand"}]}}} diff --git a/plugins/antigravity-provider/test/fixtures/tools-default.capture.ndjson b/plugins/antigravity-provider/test/fixtures/tools-default.capture.ndjson new file mode 100644 index 0000000000..8c57e4e549 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/tools-default.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", "", "--disable-slash-commands"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 21620, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/tools-default.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/tools-default.stderr.ndjson new file mode 100644 index 0000000000..4e24b2cb59 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/tools-default.stderr.ndjson @@ -0,0 +1 @@ +{"t_ms": 21540, "stream": "stderr", "data": "jetski: no output produced — a tool required the \"command\" permission that headless mode cannot prompt for, so it was auto-denied. Add an allow-rule under permissions.allow in settings.json (e.g. command()). Alternatively, re-run with --dangerously-skip-permissions to auto-approve all tools."} diff --git a/plugins/antigravity-provider/test/fixtures/tools-default.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/tools-default.stdin.ndjson new file mode 100644 index 0000000000..371a7bda84 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/tools-default.stdin.ndjson @@ -0,0 +1,3 @@ +{"t_ms": 297, "stream": "stdin", "data": {"event": "user", "message": {"role": "user", "content": "Use view_file to read /input.txt. Report its contents. Do not use a shell."}}} +{"t_ms": 6781, "stream": "stdin", "data": {"event": "user", "message": {"role": "user", "content": "Use write_to_file to create /default.txt with PHASE0_WRITE. Then use replace_file_content to change PHASE0_WRITE to PHASE0_EDIT. No shell. Do not ask me questions."}}} +{"t_ms": 17022, "stream": "stdin", "data": {"event": "user", "message": {"role": "user", "content": "Use run_command exactly once to run printf PHASE0_SHELL in . Report the output. Do not ask me questions."}}} diff --git a/plugins/antigravity-provider/test/fixtures/tools-default.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/tools-default.stdout.ndjson new file mode 100644 index 0000000000..dedb01d707 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/tools-default.stdout.ndjson @@ -0,0 +1,41 @@ +{"t_ms": 297, "stream": "stdout", "data": {"event": "init", "conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "request-review"}}} +{"t_ms": 326, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 4079, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 1, "state": "DONE", "step_type": "agent_response", "duration_seconds": 3.738430576, "usage": {"input_tokens": 11257, "output_tokens": 149, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11406}}}} +{"t_ms": 4079, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 2, "state": "ACTIVE", "step_type": "tool", "tool_name": "view_file", "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/input.txt"}}}}} +{"t_ms": 4079, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 2, "state": "DONE", "step_type": "tool", "tool_name": "view_file", "duration_seconds": 0.002338062, "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/input.txt"}, "output": "2 lines, 19 bytes"}}}} +{"t_ms": 6096, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "The contents"}}} +{"t_ms": 6223, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " of [`/worksp"}}} +{"t_ms": 6248, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ace/input.tx"}}} +{"t_ms": 6274, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "t`](file:///tmp/agy"}}} +{"t_ms": 6350, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "-p0/workspace/input.txt) are:\n\n```te"}}} +{"t_ms": 6375, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "xt\nPHASE0_RE"}}} +{"t_ms": 6400, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "AD_MARKER"}}} +{"t_ms": 6451, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "\n```"}}} +{"t_ms": 6781, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 3, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 2.698156575, "usage": {"input_tokens": 11478, "output_tokens": 49, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11527}}}} +{"t_ms": 6781, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "status": "SUCCESS", "response": "The contents of [`/input.txt`](file:///input.txt) are:\n\n```text\nPHASE0_READ_MARKER\n```\n", "duration_seconds": 6.466229654, "num_turns": 1, "usage": {"input_tokens": 22735, "output_tokens": 198, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 22933}}}} +{"t_ms": 6807, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 4, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 10766, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 5, "state": "DONE", "step_type": "agent_response", "duration_seconds": 3.966197803, "usage": {"input_tokens": 11646, "output_tokens": 171, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11817}}}} +{"t_ms": 10766, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 6, "state": "ACTIVE", "step_type": "tool", "tool_name": "write_to_file", "tool_info": {"name": "write_to_file", "parameters": {"TargetFile": "/default.txt"}}}}} +{"t_ms": 10791, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 6, "state": "DONE", "step_type": "tool", "tool_name": "write_to_file", "duration_seconds": 0.003040074, "tool_info": {"name": "write_to_file", "parameters": {"TargetFile": "/default.txt"}}}}} +{"t_ms": 13910, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 7, "state": "DONE", "step_type": "agent_response", "duration_seconds": 3.136362179, "usage": {"input_tokens": 11813, "output_tokens": 116, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11929}}}} +{"t_ms": 13911, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 8, "state": "ACTIVE", "step_type": "tool", "tool_name": "replace_file_content", "tool_info": {"name": "replace_file_content", "parameters": {"TargetFile": "/default.txt"}}}}} +{"t_ms": 13911, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 8, "state": "DONE", "step_type": "tool", "tool_name": "replace_file_content", "duration_seconds": 0.003396671, "tool_info": {"name": "replace_file_content", "parameters": {"TargetFile": "/default.txt"}}}}} +{"t_ms": 15904, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "I"}}} +{"t_ms": 16032, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " have create"}}} +{"t_ms": 16057, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "d [`/workspac"}}} +{"t_ms": 16083, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "e/default.txt`](file:///"}}} +{"t_ms": 16159, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "tmp/agy-p0/w"}}} +{"t_ms": 16184, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "orkspace/default.txt) wi"}}} +{"t_ms": 16210, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "th `PHASE0_WRITE` and th"}}} +{"t_ms": 16235, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "en updated i"}}} +{"t_ms": 16261, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "t to"}}} +{"t_ms": 16286, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " `PHASE0_EDI"}}} +{"t_ms": 16312, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "T` using `replace_file_c"}}} +{"t_ms": 16337, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ontent`."}}} +{"t_ms": 17022, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 9, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 3.099484037, "usage": {"input_tokens": 12103, "output_tokens": 61, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12164}}}} +{"t_ms": 17022, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "status": "SUCCESS", "response": "I have created [`/default.txt`](file:///default.txt) with `PHASE0_WRITE` and then updated it to `PHASE0_EDIT` using `replace_file_content`.\n", "duration_seconds": 16.710782303, "num_turns": 2, "usage": {"input_tokens": 58297, "output_tokens": 546, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 58843}}}} +{"t_ms": 17049, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 10, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 21540, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 11, "state": "DONE", "step_type": "agent_response", "duration_seconds": 4.490021482, "usage": {"input_tokens": 12263, "output_tokens": 370, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12633}}}} +{"t_ms": 21540, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 12, "state": "ACTIVE", "step_type": "tool", "tool_name": "run_command", "tool_info": {"name": "run_command", "parameters": {"CommandLine": "printf PHASE0_SHELL"}}}}} +{"t_ms": 21540, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "step_index": 12, "state": "ERROR", "step_type": "tool", "tool_name": "run_command", "duration_seconds": 0.002821636, "tool_info": {"name": "run_command", "parameters": {"CommandLine": "printf PHASE0_SHELL"}, "error": {"type": "TOOL_ERROR", "message": "permission check failed for command \"printf PHASE0_SHELL\": user denied permission to run command:\nprintf PHASE0_SHELL\nDo not attempt to circumvent this denial by rephrasing the command, using alternative tools/scripts (e.g. python, sh, curl), or accessing the same target resource. Proceed without performing this action."}}}}} +{"t_ms": 21540, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "6d13e726-1498-4995-aa81-7a0851679fb3", "status": "SUCCESS", "response": "", "duration_seconds": 21.23332446, "num_turns": 3, "usage": {"input_tokens": 70560, "output_tokens": 916, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 71476}, "denied_actions": [{"action": "command", "display_name": "RunCommand"}]}}} diff --git a/plugins/antigravity-provider/test/fixtures/tools-plan.capture.ndjson b/plugins/antigravity-provider/test/fixtures/tools-plan.capture.ndjson new file mode 100644 index 0000000000..4a1151d083 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/tools-plan.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", "", "--disable-slash-commands", "--mode", "plan"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 16560, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/tools-plan.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/tools-plan.stderr.ndjson new file mode 100644 index 0000000000..be57aac639 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/tools-plan.stderr.ndjson @@ -0,0 +1,4 @@ +{"t_ms": 292, "stream": "stderr", "data": "warning: --mode plan has no effect while slash command expansion is disabled."} +{"t_ms": 6705, "stream": "stderr", "data": "warning: --mode plan has no effect while slash command expansion is disabled."} +{"t_ms": 13456, "stream": "stderr", "data": "warning: --mode plan has no effect while slash command expansion is disabled."} +{"t_ms": 16400, "stream": "stderr", "data": "jetski: no output produced — a tool required the \"command\" permission that headless mode cannot prompt for, so it was auto-denied. Add an allow-rule under permissions.allow in settings.json (e.g. command()). Alternatively, re-run with --dangerously-skip-permissions to auto-approve all tools."} diff --git a/plugins/antigravity-provider/test/fixtures/tools-plan.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/tools-plan.stdin.ndjson new file mode 100644 index 0000000000..c4d09c8ae7 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/tools-plan.stdin.ndjson @@ -0,0 +1,3 @@ +{"t_ms": 292, "stream": "stdin", "data": {"event": "user", "message": {"role": "user", "content": "Use view_file to read /input.txt. Report its contents. Do not use a shell."}}} +{"t_ms": 6705, "stream": "stdin", "data": {"event": "user", "message": {"role": "user", "content": "Use write_to_file to create /plan.txt with PHASE0_WRITE. Then use replace_file_content to change PHASE0_WRITE to PHASE0_EDIT. No shell. Do not ask me questions."}}} +{"t_ms": 13455, "stream": "stdin", "data": {"event": "user", "message": {"role": "user", "content": "Use run_command exactly once to run printf PHASE0_SHELL in . Report the output. Do not ask me questions."}}} diff --git a/plugins/antigravity-provider/test/fixtures/tools-plan.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/tools-plan.stdout.ndjson new file mode 100644 index 0000000000..473540451c --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/tools-plan.stdout.ndjson @@ -0,0 +1,36 @@ +{"t_ms": 292, "stream": "stdout", "data": {"event": "init", "conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "request-review"}}} +{"t_ms": 319, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 3576, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 1, "state": "DONE", "step_type": "agent_response", "duration_seconds": 3.249717435, "usage": {"input_tokens": 11253, "output_tokens": 339, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11592}}}} +{"t_ms": 3576, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 2, "state": "ACTIVE", "step_type": "tool", "tool_name": "view_file", "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/input.txt"}}}}} +{"t_ms": 3576, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 2, "state": "DONE", "step_type": "tool", "tool_name": "view_file", "duration_seconds": 0.002425685, "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/input.txt"}, "output": "2 lines, 19 bytes"}}}} +{"t_ms": 6197, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "The contents"}}} +{"t_ms": 6324, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " of [`/input.tx"}}} +{"t_ms": 6349, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "t`](file:///"}}} +{"t_ms": 6374, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "tmp/"}}} +{"t_ms": 6451, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "agy-p0/workspace/input.txt) are:\n\n``"}}} +{"t_ms": 6476, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "`\nPHASE0_REA"}}} +{"t_ms": 6502, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "D_MARKER\n```"}}} +{"t_ms": 6705, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 3, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 3.109263873, "usage": {"input_tokens": 11475, "output_tokens": 48, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11523}}}} +{"t_ms": 6705, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "status": "SUCCESS", "response": "The contents of [`/input.txt`](file:///input.txt) are:\n\n```\nPHASE0_READ_MARKER\n```\n", "duration_seconds": 6.388206511, "num_turns": 1, "usage": {"input_tokens": 22728, "output_tokens": 387, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 23115}}}} +{"t_ms": 6732, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 4, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 9502, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 5, "state": "DONE", "step_type": "agent_response", "duration_seconds": 2.757462001, "usage": {"input_tokens": 11642, "output_tokens": 166, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11808}}}} +{"t_ms": 9502, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 6, "state": "ACTIVE", "step_type": "tool", "tool_name": "write_to_file", "tool_info": {"name": "write_to_file", "parameters": {"TargetFile": "/plan.txt"}}}}} +{"t_ms": 9502, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 6, "state": "DONE", "step_type": "tool", "tool_name": "write_to_file", "duration_seconds": 0.003883221, "tool_info": {"name": "write_to_file", "parameters": {"TargetFile": "/plan.txt"}}}}} +{"t_ms": 11716, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 7, "state": "DONE", "step_type": "agent_response", "duration_seconds": 2.227570207, "usage": {"input_tokens": 11811, "output_tokens": 118, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11929}}}} +{"t_ms": 11716, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 8, "state": "ACTIVE", "step_type": "tool", "tool_name": "replace_file_content", "tool_info": {"name": "replace_file_content", "parameters": {"TargetFile": "/plan.txt"}}}}} +{"t_ms": 11742, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 8, "state": "DONE", "step_type": "tool", "tool_name": "replace_file_content", "duration_seconds": 0.002974983, "tool_info": {"name": "replace_file_content", "parameters": {"TargetFile": "/plan.txt"}}}}} +{"t_ms": 13151, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "[`/"}}} +{"t_ms": 13176, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "plan.txt`](file:///tmp/"}}} +{"t_ms": 13252, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "agy-p0/workspace/plan.tx"}}} +{"t_ms": 13278, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "t) was created with `PHA"}}} +{"t_ms": 13303, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "SE0_WRITE` and then upda"}}} +{"t_ms": 13329, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ted to `PHASE0_EDIT` usi"}}} +{"t_ms": 13354, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ng `replace_file_content"}}} +{"t_ms": 13379, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "`."}}} +{"t_ms": 13455, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 9, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 1.716582968, "usage": {"input_tokens": 12103, "output_tokens": 59, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12162}}}} +{"t_ms": 13455, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "status": "SUCCESS", "response": "[`/plan.txt`](file:///plan.txt) was created with `PHASE0_WRITE` and then updated to `PHASE0_EDIT` using `replace_file_content`.\n", "duration_seconds": 13.142130621, "num_turns": 2, "usage": {"input_tokens": 58284, "output_tokens": 730, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 59014}}}} +{"t_ms": 13482, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 10, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 16400, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 11, "state": "DONE", "step_type": "agent_response", "duration_seconds": 2.913339623, "usage": {"input_tokens": 12261, "output_tokens": 74, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12335}}}} +{"t_ms": 16400, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 12, "state": "ACTIVE", "step_type": "tool", "tool_name": "run_command", "tool_info": {"name": "run_command", "parameters": {"CommandLine": "printf PHASE0_SHELL"}}}}} +{"t_ms": 16400, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "step_index": 12, "state": "ERROR", "step_type": "tool", "tool_name": "run_command", "duration_seconds": 0.002793073, "tool_info": {"name": "run_command", "parameters": {"CommandLine": "printf PHASE0_SHELL"}, "error": {"type": "TOOL_ERROR", "message": "permission check failed for command \"printf PHASE0_SHELL\": user denied permission to run command:\nprintf PHASE0_SHELL\nDo not attempt to circumvent this denial by rephrasing the command, using alternative tools/scripts (e.g. python, sh, curl), or accessing the same target resource. Proceed without performing this action."}}}}} +{"t_ms": 16400, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "7e1cdc94-5651-4941-b053-7e2afe73d01e", "status": "SUCCESS", "response": "", "duration_seconds": 16.097866635, "num_turns": 3, "usage": {"input_tokens": 70545, "output_tokens": 804, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 71349}, "denied_actions": [{"action": "command", "display_name": "RunCommand"}]}}} diff --git a/plugins/antigravity-provider/test/fixtures/tools-sandbox.capture.ndjson b/plugins/antigravity-provider/test/fixtures/tools-sandbox.capture.ndjson new file mode 100644 index 0000000000..b845d3e735 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/tools-sandbox.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", "", "--disable-slash-commands", "--sandbox"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 13022, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/tools-sandbox.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/tools-sandbox.stderr.ndjson new file mode 100644 index 0000000000..624a99a74c --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/tools-sandbox.stderr.ndjson @@ -0,0 +1 @@ +{"t_ms": 12723, "stream": "stderr", "data": "jetski: no output produced — a tool required the \"command\" permission that headless mode cannot prompt for, so it was auto-denied. Add an allow-rule under permissions.allow in settings.json (e.g. command()). Alternatively, re-run with --dangerously-skip-permissions to auto-approve all tools."} diff --git a/plugins/antigravity-provider/test/fixtures/tools-sandbox.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/tools-sandbox.stdin.ndjson new file mode 100644 index 0000000000..9ebac338e8 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/tools-sandbox.stdin.ndjson @@ -0,0 +1,3 @@ +{"t_ms": 320, "stream": "stdin", "data": {"event": "user", "message": {"role": "user", "content": "Use view_file to read /input.txt. Report its contents. Do not use a shell."}}} +{"t_ms": 4959, "stream": "stdin", "data": {"event": "user", "message": {"role": "user", "content": "Use write_to_file to create /sandbox.txt with PHASE0_WRITE. Then use replace_file_content to change PHASE0_WRITE to PHASE0_EDIT. No shell. Do not ask me questions."}}} +{"t_ms": 10581, "stream": "stdin", "data": {"event": "user", "message": {"role": "user", "content": "Use run_command exactly once to run printf PHASE0_SHELL in . Report the output. Do not ask me questions."}}} diff --git a/plugins/antigravity-provider/test/fixtures/tools-sandbox.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/tools-sandbox.stdout.ndjson new file mode 100644 index 0000000000..6d63907468 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/tools-sandbox.stdout.ndjson @@ -0,0 +1,40 @@ +{"t_ms": 319, "stream": "stdout", "data": {"event": "init", "conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "request-review"}}} +{"t_ms": 346, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 3169, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 1, "state": "DONE", "step_type": "agent_response", "duration_seconds": 2.806171145, "usage": {"input_tokens": 12031, "output_tokens": 307, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12338}}}} +{"t_ms": 3169, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 2, "state": "ACTIVE", "step_type": "tool", "tool_name": "view_file", "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/input.txt"}}}}} +{"t_ms": 3169, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 2, "state": "DONE", "step_type": "tool", "tool_name": "view_file", "duration_seconds": 0.002973019, "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/input.txt"}, "output": "2 lines, 19 bytes"}}}} +{"t_ms": 4705, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "The contents"}}} +{"t_ms": 4755, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " of [input.txt](file:///"}}} +{"t_ms": 4781, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "tmp/agy-p0/w"}}} +{"t_ms": 4806, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "orkspace/input"}}} +{"t_ms": 4857, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": ".txt) are:\n\n"}}} +{"t_ms": 4882, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "```text\nPHASE0_READ_MARK"}}} +{"t_ms": 4908, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ER\n```"}}} +{"t_ms": 4959, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 3, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 1.791180207, "usage": {"input_tokens": 12253, "output_tokens": 39, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12292}}}} +{"t_ms": 4959, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "status": "SUCCESS", "response": "The contents of [input.txt](file:///input.txt) are:\n\n```text\nPHASE0_READ_MARKER\n```\n", "duration_seconds": 4.624371092, "num_turns": 1, "usage": {"input_tokens": 24284, "output_tokens": 346, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 24630}}}} +{"t_ms": 4986, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 4, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 6674, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 5, "state": "DONE", "step_type": "agent_response", "duration_seconds": 1.678518989, "usage": {"input_tokens": 12411, "output_tokens": 78, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12489}}}} +{"t_ms": 6674, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 6, "state": "ACTIVE", "step_type": "tool", "tool_name": "write_to_file", "tool_info": {"name": "write_to_file", "parameters": {"TargetFile": "/sandbox.txt"}}}}} +{"t_ms": 6674, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 6, "state": "DONE", "step_type": "tool", "tool_name": "write_to_file", "duration_seconds": 0.002765401, "tool_info": {"name": "write_to_file", "parameters": {"TargetFile": "/sandbox.txt"}}}}} +{"t_ms": 8939, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 7, "state": "DONE", "step_type": "agent_response", "duration_seconds": 2.278065329, "usage": {"input_tokens": 12577, "output_tokens": 118, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12695}}}} +{"t_ms": 8939, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 8, "state": "ACTIVE", "step_type": "tool", "tool_name": "replace_file_content", "tool_info": {"name": "replace_file_content", "parameters": {"TargetFile": "/sandbox.txt"}}}}} +{"t_ms": 8965, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 8, "state": "DONE", "step_type": "tool", "tool_name": "replace_file_content", "duration_seconds": 0.002992846, "tool_info": {"name": "replace_file_content", "parameters": {"TargetFile": "/sandbox.txt"}}}}} +{"t_ms": 10124, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "### Summary"}}} +{"t_ms": 10251, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " of Changes\n\n1. **Created File**: Us"}}} +{"t_ms": 10276, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ed `write_to"}}} +{"t_ms": 10302, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "_file` to create"}}} +{"t_ms": 10378, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " [sandbox.tx"}}} +{"t_ms": 10403, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "t](file:///wo"}}} +{"t_ms": 10428, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "rkspace/sandbox.txt) wit"}}} +{"t_ms": 10454, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "h the conten"}}} +{"t_ms": 10479, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "t `PHASE0_WRITE`.\n2. **Updated Con"}}} +{"t_ms": 10504, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "tent**: Used `replace_fi"}}} +{"t_ms": 10530, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "le_content` to replace `"}}} +{"t_ms": 10555, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "PHASE0_WRITE` with `PHAS"}}} +{"t_ms": 10581, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 9, "state": "DONE", "step_type": "agent_response", "text_delta": "E0_EDIT`.\n", "duration_seconds": 1.630993868, "usage": {"input_tokens": 12869, "output_tokens": 83, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12952}}}} +{"t_ms": 10581, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "status": "SUCCESS", "response": "### Summary of Changes\n\n1. **Created File**: Used `write_to_file` to create [sandbox.txt](file:///sandbox.txt) with the content `PHASE0_WRITE`.\n2. **Updated Content**: Used `replace_file_content` to replace `PHASE0_WRITE` with `PHASE0_EDIT`.\n", "duration_seconds": 10.25241531, "num_turns": 2, "usage": {"input_tokens": 62141, "output_tokens": 625, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 62766}}}} +{"t_ms": 10608, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 10, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 12723, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 11, "state": "DONE", "step_type": "agent_response", "duration_seconds": 2.117119481, "usage": {"input_tokens": 13051, "output_tokens": 80, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 13131}}}} +{"t_ms": 12723, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 12, "state": "ACTIVE", "step_type": "tool", "tool_name": "run_command", "tool_info": {"name": "run_command", "parameters": {"CommandLine": "printf PHASE0_SHELL"}}}}} +{"t_ms": 12723, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "step_index": 12, "state": "ERROR", "step_type": "tool", "tool_name": "run_command", "duration_seconds": 0.00280781, "tool_info": {"name": "run_command", "parameters": {"CommandLine": "printf PHASE0_SHELL"}, "error": {"type": "TOOL_ERROR", "message": "permission check failed for command \"printf PHASE0_SHELL\": user denied permission to run command:\nprintf PHASE0_SHELL\nDo not attempt to circumvent this denial by rephrasing the command, using alternative tools/scripts (e.g. python, sh, curl), or accessing the same target resource. Proceed without performing this action."}}}}} +{"t_ms": 12723, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "75d9e35e-6354-48a9-b055-d0dd752c47b9", "status": "SUCCESS", "response": "", "duration_seconds": 12.395964579, "num_turns": 3, "usage": {"input_tokens": 75192, "output_tokens": 705, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 75897}, "denied_actions": [{"action": "command", "display_name": "RunCommand"}]}}} diff --git a/plugins/antigravity-provider/test/fixtures/tools-skip-permissions.capture.ndjson b/plugins/antigravity-provider/test/fixtures/tools-skip-permissions.capture.ndjson new file mode 100644 index 0000000000..dd49b19784 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/tools-skip-permissions.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--input-format", "stream-json", "--output-format", "stream-json", "--add-dir", "", "--disable-slash-commands", "--dangerously-skip-permissions"], "cwd": "", "home": "", "settings": {"modelProvider": "gemini"}, "env": {"GEMINI_API_KEY": "", "GOOGLE_GEMINI_BASE_URL": "http://127.0.0.1:"}}} +{"t_ms": 17771, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/tools-skip-permissions.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/tools-skip-permissions.stderr.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/tools-skip-permissions.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/tools-skip-permissions.stdin.ndjson new file mode 100644 index 0000000000..f91311bcf7 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/tools-skip-permissions.stdin.ndjson @@ -0,0 +1,3 @@ +{"t_ms": 292, "stream": "stdin", "data": {"event": "user", "message": {"role": "user", "content": "Use view_file to read /input.txt. Report its contents. Do not use a shell."}}} +{"t_ms": 5051, "stream": "stdin", "data": {"event": "user", "message": {"role": "user", "content": "Use write_to_file to create /skip-permissions.txt with PHASE0_WRITE. Then use replace_file_content to change PHASE0_WRITE to PHASE0_EDIT. No shell. Do not ask me questions."}}} +{"t_ms": 11704, "stream": "stdin", "data": {"event": "user", "message": {"role": "user", "content": "Use run_command exactly once to run printf PHASE0_SHELL in . Report the output. Do not ask me questions."}}} diff --git a/plugins/antigravity-provider/test/fixtures/tools-skip-permissions.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/tools-skip-permissions.stdout.ndjson new file mode 100644 index 0000000000..9d2f1fbafa --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/tools-skip-permissions.stdout.ndjson @@ -0,0 +1,42 @@ +{"t_ms": 291, "stream": "stdout", "data": {"event": "init", "conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "init": {"cwd": "", "tools": ["ask_custom_permission", "ask_permission", "ask_question", "browser_click_element", "browser_drag_pixel_to_pixel", "browser_get_dom", "browser_get_network_request", "browser_input", "browser_list_network_requests", "browser_mouse_down", "browser_mouse_up", "browser_move_mouse", "browser_press_key", "browser_refresh_page", "browser_resize_window", "browser_scroll", "browser_scroll_dom", "browser_select_option", "browser_subagent", "call_mcp_tool", "capture_browser_console_logs", "capture_browser_screenshot", "click_browser_pixel", "command_status", "define_subagent", "delete_knowledge", "execute_browser_javascript", "find_by_name", "finish", "generate_image", "grep_search", "invoke_subagent", "list_browser_pages", "list_dir", "list_permissions", "list_resources", "manage_inbox", "manage_subagents", "manage_task", "multi_replace_file_content", "notebook_edit", "notebook_execution", "open_browser_url", "read_browser_page", "read_resource", "read_url_content", "replace_file_content", "run_command", "schedule", "search_web", "sed_file", "send_command_input", "send_message", "view_file", "wait", "wait_5_seconds", "write_to_file"], "permission_mode": "always-proceed"}}} +{"t_ms": 318, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 0, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 2658, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 1, "state": "DONE", "step_type": "agent_response", "duration_seconds": 2.317552401, "usage": {"input_tokens": 11263, "output_tokens": 150, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11413}}}} +{"t_ms": 2658, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 2, "state": "ACTIVE", "step_type": "tool", "tool_name": "view_file", "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/input.txt"}}}}} +{"t_ms": 2658, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 2, "state": "DONE", "step_type": "tool", "tool_name": "view_file", "duration_seconds": 0.002646148, "tool_info": {"name": "view_file", "parameters": {"AbsolutePath": "/input.txt"}, "output": "2 lines, 19 bytes"}}}} +{"t_ms": 4645, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "The contents"}}} +{"t_ms": 4721, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " of [input.txt](file:///"}}} +{"t_ms": 4746, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "tmp/agy-p0/workspace/inp"}}} +{"t_ms": 4772, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ut"}}} +{"t_ms": 4848, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": ".txt) are:\n\n```\nPHASE0_READ_MARKER\n`"}}} +{"t_ms": 4873, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 3, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "``"}}} +{"t_ms": 5025, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 3, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 2.386909321, "usage": {"input_tokens": 11484, "output_tokens": 38, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11522}}}} +{"t_ms": 5051, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "status": "SUCCESS", "response": "The contents of [input.txt](file:///input.txt) are:\n\n```\nPHASE0_READ_MARKER\n```\n", "duration_seconds": 4.732286256, "num_turns": 1, "usage": {"input_tokens": 22747, "output_tokens": 188, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 22935}}}} +{"t_ms": 5078, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 4, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 7346, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 5, "state": "DONE", "step_type": "agent_response", "duration_seconds": 2.260780071, "usage": {"input_tokens": 11643, "output_tokens": 152, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11795}}}} +{"t_ms": 7346, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 6, "state": "ACTIVE", "step_type": "tool", "tool_name": "write_to_file", "tool_info": {"name": "write_to_file", "parameters": {"TargetFile": "/skip-permissions.txt"}}}}} +{"t_ms": 7346, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 6, "state": "DONE", "step_type": "tool", "tool_name": "write_to_file", "duration_seconds": 0.003788063, "tool_info": {"name": "write_to_file", "parameters": {"TargetFile": "/skip-permissions.txt"}}}}} +{"t_ms": 10113, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 7, "state": "DONE", "step_type": "agent_response", "duration_seconds": 2.769162417, "usage": {"input_tokens": 11812, "output_tokens": 117, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 11929}}}} +{"t_ms": 10113, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 8, "state": "ACTIVE", "step_type": "tool", "tool_name": "replace_file_content", "tool_info": {"name": "replace_file_content", "parameters": {"TargetFile": "/skip-permissions.txt"}}}}} +{"t_ms": 10113, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 8, "state": "DONE", "step_type": "tool", "tool_name": "replace_file_content", "duration_seconds": 0.004067275, "tool_info": {"name": "replace_file_content", "parameters": {"TargetFile": "/skip-permissions.txt"}}}}} +{"t_ms": 11349, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "I have"}}} +{"t_ms": 11450, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " created [skip-permissio"}}} +{"t_ms": 11476, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ns.txt](file"}}} +{"t_ms": 11501, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "://"}}} +{"t_ms": 11526, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "/skip"}}} +{"t_ms": 11552, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "-permissions.txt) with `"}}} +{"t_ms": 11577, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "PHASE0_WRITE` and then u"}}} +{"t_ms": 11603, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "pdated its c"}}} +{"t_ms": 11628, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ontent to `PHASE0_EDIT` using `repla"}}} +{"t_ms": 11653, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ce_file_cont"}}} +{"t_ms": 11678, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 9, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "ent`."}}} +{"t_ms": 11704, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 9, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 1.563629905, "usage": {"input_tokens": 12105, "output_tokens": 56, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12161}}}} +{"t_ms": 11704, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "status": "SUCCESS", "response": "I have created [skip-permissions.txt](file:///skip-permissions.txt) with `PHASE0_WRITE` and then updated its content to `PHASE0_EDIT` using `replace_file_content`.\n", "duration_seconds": 11.386053268, "num_turns": 2, "usage": {"input_tokens": 58307, "output_tokens": 513, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 58820}}}} +{"t_ms": 11731, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 10, "state": "DONE", "step_type": "user_input"}}} +{"t_ms": 16142, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 11, "state": "DONE", "step_type": "agent_response", "duration_seconds": 4.411191659, "usage": {"input_tokens": 12260, "output_tokens": 390, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12650}}}} +{"t_ms": 16143, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 12, "state": "ACTIVE", "step_type": "tool", "tool_name": "run_command", "tool_info": {"name": "run_command", "parameters": {"CommandLine": "printf PHASE0_SHELL"}}}}} +{"t_ms": 16143, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 12, "state": "DONE", "step_type": "tool", "tool_name": "run_command", "duration_seconds": 0.007492259, "tool_info": {"name": "run_command", "parameters": {"CommandLine": "printf PHASE0_SHELL"}, "output": "PHASE0_SHELL"}}}} +{"t_ms": 17501, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 13, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "The command"}}} +{"t_ms": 17578, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 13, "state": "ACTIVE", "step_type": "agent_response", "text_delta": " output is:\n"}}} +{"t_ms": 17603, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 13, "state": "ACTIVE", "step_type": "agent_response", "text_delta": "\n```\nPHASE0_SHELL\n```"}}} +{"t_ms": 17680, "stream": "stdout", "data": {"event": "step_update", "step_update": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "step_index": 13, "state": "DONE", "step_type": "agent_response", "text_delta": "\n", "duration_seconds": 1.518696431, "usage": {"input_tokens": 12396, "output_tokens": 16, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 12412}}}} +{"t_ms": 17680, "stream": "stdout", "data": {"event": "result", "result": {"conversation_id": "7e55929a-de5b-4564-af80-76cff39e16f9", "status": "SUCCESS", "response": "The command output is:\n\n```\nPHASE0_SHELL\n```\n", "duration_seconds": 17.367491474, "num_turns": 3, "usage": {"input_tokens": 82963, "output_tokens": 919, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 83882}}}} diff --git a/plugins/antigravity-provider/test/fixtures/version.capture.ndjson b/plugins/antigravity-provider/test/fixtures/version.capture.ndjson new file mode 100644 index 0000000000..ac8e605e77 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/version.capture.ndjson @@ -0,0 +1,2 @@ +{"t_ms": 0, "stream": "capture", "data": {"argv": ["/.local/opt/agy/1.2.13/agy", "--version"], "cwd": "", "home": "", "settings": null, "env": {}}} +{"t_ms": 284, "stream": "capture", "data": {"exit_code": 0}} diff --git a/plugins/antigravity-provider/test/fixtures/version.stderr.ndjson b/plugins/antigravity-provider/test/fixtures/version.stderr.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/version.stdin.ndjson b/plugins/antigravity-provider/test/fixtures/version.stdin.ndjson new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/antigravity-provider/test/fixtures/version.stdout.ndjson b/plugins/antigravity-provider/test/fixtures/version.stdout.ndjson new file mode 100644 index 0000000000..ce8e23e783 --- /dev/null +++ b/plugins/antigravity-provider/test/fixtures/version.stdout.ndjson @@ -0,0 +1 @@ +{"t_ms": 269, "stream": "stdout", "data": "1.2.13"} diff --git a/public-docs/providers.md b/public-docs/providers.md index bf9e82a22c..6de304a019 100644 --- a/public-docs/providers.md +++ b/public-docs/providers.md @@ -8,7 +8,7 @@ category: Providers # Providers -Paseo doesn't ship its own coding agent. It launches and supervises **existing CLIs you've already installed and authenticated**, Claude Code, Codex, OpenCode, Muse Code, Cursor, Gemini, and the rest. Your subscriptions, your config, your skills, your MCP servers all stay intact. Paseo just gives you a UI, a CLI, a relay, and orchestration on top. +Paseo doesn't ship its own coding agent. It launches and supervises **existing CLIs you've already installed and authenticated**, Claude Code, Codex, OpenCode, Antigravity, Muse Code, Cursor, Gemini, and the rest. Your subscriptions, your config, your skills, your MCP servers all stay intact. Paseo just gives you a UI, a CLI, a relay, and orchestration on top. ## Mental model @@ -16,7 +16,7 @@ A provider is the contract between Paseo and one external agent CLI: how to laun ## Two tiers -- **Native support**, Paseo ships a bundled adapter for the major agents (Claude Code, Codex, OpenCode, Pi, [Muse Code](/docs/muse-code)). Auto-discovered when the underlying CLI is installed, with mode metadata and voice support where applicable. +- **Native support**, Paseo ships a bundled adapter for the major agents (Claude Code, Codex, OpenCode, Pi, [Antigravity](/docs/supported-providers#antigravity), [Muse Code](/docs/muse-code)). Auto-discovered when the underlying CLI is installed, with mode metadata and voice support where applicable. - **ACP catalog**, any agent speaking the [Agent Client Protocol](https://agentclientprotocol.com) is supported through a generic adapter. Paseo ships a curated catalog of one-click installs (Cursor, Gemini, GitHub Copilot, Hermes, Kimi, Qwen Code, and 25+ more), and you can add any other ACP agent yourself. Either way, **you install the underlying CLI**. Paseo runs it. diff --git a/public-docs/supported-providers.md b/public-docs/supported-providers.md index 98db77d07e..f2b1370b68 100644 --- a/public-docs/supported-providers.md +++ b/public-docs/supported-providers.md @@ -18,8 +18,46 @@ Work out of the box once the underlying CLI is installed and authenticated. - [Codex](/docs/codex). OpenAI's workspace agent with sandbox controls and optional network access. - [OpenCode](https://opencode.ai/). Open-source coding assistant with multi-provider model support. - [Pi](https://pi.dev). Minimal terminal-based coding agent with multi-provider LLM support. +- [Antigravity](#antigravity). Uses your installed `agy` CLI through a built-in provider plugin. - [Muse Code](/docs/muse-code). Meta's terminal coding agent, bundled as a provider plugin. +### Antigravity + +Install the Antigravity CLI (`agy`) on your daemon host, then run `agy` once to sign in. Pick +**Antigravity** in Paseo. The provider ships with Paseo; no separate plugin install is required. + +**Full access** is the only permission mode. Antigravity cannot ask for permission when another +app drives it, so Paseo starts `agy` with `--dangerously-skip-permissions`. Every tool call, +including shell commands, runs without asking. Each session starts with a warning explaining +this, including when you reopen a chat. Antigravity's own policy can still deny +an action. + +After a daemon restart, Antigravity remembers the conversation, but Paseo's transcript starts +fresh because `agy` does not replay its history. + +This integration uses your installed CLI and sign-in. It is not endorsed by Google. + +Paseo MCP tools and steering are unavailable. Your own `agy` MCP servers still work. Attached +images are passed as file references for the agent to read. + +If `agy` is outside your daemon's PATH, override its command in `~/.paseo/config.json`. Use `env` +for provider-specific environment variables: + +```json +{ + "agents": { + "providers": { + "antigravity": { + "command": ["/absolute/path/to/agy"], + "env": { "EXAMPLE_VARIABLE": "value" } + } + } + } +} +``` + +See [Custom providers](/docs/custom-providers) for command and environment overrides. + ## ACP catalog Pick any of these from the in-app provider catalog. Each entry is a one-click install of the underlying CLI. From e0a0cf990d27f9a40cb1ef2fc490a1d2d7a7e4ef Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Thu, 1 Oct 2026 01:15:34 +0200 Subject: [PATCH 036/100] Ask OpenCode before running a Paseo tool (#5801) OpenCode checks permissions for its own tools and wraps MCP tools in an ask, but leaves plugin tools to call ctx.ask themselves. The bridge plugin never did, so an "ask" rule for paseo_* tools never produced a prompt and the tool ran. The bridge now asks with the tool's catalog name before dispatching, the same shape OpenCode uses for MCP tools. With no paseo_* rule OpenCode's default allow applies, so nothing new prompts. Fixes #5324 Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> Co-authored-by: Yuval Marcus --- .../opencode-bridge.local.e2e.test.ts | 70 +++++++++++++++++++ .../providers/opencode/bridge-plugin.mjs | 7 ++ .../agent/providers/opencode/bridge.test.ts | 26 ++++++- 3 files changed, 102 insertions(+), 1 deletion(-) diff --git a/packages/server/src/server/agent/providers/opencode-bridge.local.e2e.test.ts b/packages/server/src/server/agent/providers/opencode-bridge.local.e2e.test.ts index a441cebcb5..d3d295cc8e 100644 --- a/packages/server/src/server/agent/providers/opencode-bridge.local.e2e.test.ts +++ b/packages/server/src/server/agent/providers/opencode-bridge.local.e2e.test.ts @@ -8,6 +8,10 @@ import { expect, test } from "vitest"; import { execCommand } from "../../../utils/spawn.js"; import { createTestLogger } from "../../../test-utils/test-logger.js"; +import { + getRealProviderConfig, + getRealProviderRuntimeSettings, +} from "../../daemon-e2e/real-provider-test-config.js"; import type { PaseoToolCatalog } from "../tools/types.js"; import { OpenCodeAgentClient } from "./opencode-agent.js"; import { OpenCodeV2AgentClient } from "./opencode/v2/agent.js"; @@ -199,6 +203,72 @@ test("real OpenCode server shares one process while shell.env stays session-scop } }, 240_000); +test("real OpenCode server asks before running a Paseo tool with an ask rule", async () => { + const root = await mkdtemp(path.join(os.tmpdir(), "paseo-opencode-ask-")); + const cwd = path.join(root, "repo"); + const logger = createTestLogger(); + const bridge = new OpenCodeBridge({ paseoHome: root, logger }); + await bridge.start(); + const catalog = createCallerCatalog("ask-rule-agent"); + bridge.setManifestCatalog(catalog); + const realSettings = getRealProviderRuntimeSettings("opencode"); + const runtimeSettings = { + ...realSettings, + env: { + ...realSettings.env, + OPENCODE_CONFIG_CONTENT: JSON.stringify({ permission: { "paseo_*": "ask" } }), + }, + }; + const manager = new OpenCodeServerManager({ + logger, + runtimeSettings, + resolveHomeDir: () => root, + decorateServerEnv: (env) => bridge.decorateServerEnv(env), + }); + const client = new OpenCodeAgentClient(logger, runtimeSettings, { + serverManager: manager, + bridge, + }); + let session: Awaited> | undefined; + + try { + await mkdir(cwd); + session = await client.createSession( + { ...getRealProviderConfig("opencode"), cwd }, + { agentId: "ask-rule-agent", paseoTools: catalog }, + { persistSession: false }, + ); + const asked: string[] = []; + const completedTools: string[] = []; + const activeSession = session; + session.subscribe((event) => { + if (event.type === "permission_requested") { + asked.push(event.request.name); + void activeSession.respondToPermission(event.request.id, { behavior: "allow" }); + } + if ( + event.type === "timeline" && + event.item.type === "tool_call" && + event.item.status === "completed" + ) { + completedTools.push(event.item.name); + } + }); + + await session.run( + "Call the paseo_report_caller_agent_id tool once, then reply with its exact result. Do not use any other tools.", + ); + + expect(completedTools).toContain("paseo_report_caller_agent_id"); + expect(asked).toContain("paseo_report_caller_agent_id"); + } finally { + await session?.close(); + await manager.shutdown(); + await bridge.close(); + await rm(root, { recursive: true, force: true }); + } +}, 180_000); + function createCallerCatalog(callerAgentId: string): PaseoToolCatalog { const tool = { name: "report_caller_agent_id", diff --git a/packages/server/src/server/agent/providers/opencode/bridge-plugin.mjs b/packages/server/src/server/agent/providers/opencode/bridge-plugin.mjs index 058ade60f8..82e4166492 100644 --- a/packages/server/src/server/agent/providers/opencode/bridge-plugin.mjs +++ b/packages/server/src/server/agent/providers/opencode/bridge-plugin.mjs @@ -33,6 +33,13 @@ export default async function paseoPlugin(input, options) { description: definition.description, args: jsonSchemaObjectToZodShape(definition.inputSchema), execute: async (args, context) => { + // OpenCode leaves permission checks to plugin tools, so ask it the way it asks for MCP tools. + await context.ask({ + permission: `paseo_${definition.name}`, + patterns: ["*"], + always: ["*"], + metadata: {}, + }); let result; try { result = await request( diff --git a/packages/server/src/server/agent/providers/opencode/bridge.test.ts b/packages/server/src/server/agent/providers/opencode/bridge.test.ts index 2df81b3241..13cd6aa4d8 100644 --- a/packages/server/src/server/agent/providers/opencode/bridge.test.ts +++ b/packages/server/src/server/agent/providers/opencode/bridge.test.ts @@ -102,6 +102,12 @@ describe("OpenCodeBridge", () => { const paseoHome = await mkdtemp(path.join(tmpdir(), "paseo-opencode-bridge-")); temporaryDirectories.push(paseoHome); const catalog = createCatalog(); + const executedInputs: unknown[] = []; + const executeTool = catalog.executeTool; + catalog.executeTool = async (name, input, context) => { + executedInputs.push(input); + return await executeTool(name, input, context); + }; const bridge = new OpenCodeBridge({ paseoHome, logger: createTestLogger() }); await bridge.start(); bridge.setManifestCatalog(catalog); @@ -173,12 +179,30 @@ describe("OpenCodeBridge", () => { token: plugin.token, }, ); + const asked: unknown[] = []; await expect( hooks.tool.paseo_echo_context.execute( { value: "through bundled plugin" }, - { sessionID: "ses_one" }, + { sessionID: "ses_one", ask: async (request: unknown) => void asked.push(request) }, ), ).resolves.toMatchObject({ output: "through bundled plugin" }); + expect(asked).toEqual([ + { permission: "paseo_echo_context", patterns: ["*"], always: ["*"], metadata: {} }, + ]); + + executedInputs.length = 0; + await expect( + hooks.tool.paseo_echo_context.execute( + { value: "rejected" }, + { + sessionID: "ses_one", + ask: async () => { + throw new Error("The user rejected permission to use this specific tool call."); + }, + }, + ), + ).rejects.toThrow("rejected permission"); + expect(executedInputs).toEqual([]); release(); const pluginError = vi.spyOn(console, "error").mockImplementation(() => undefined); From 4893629ff5ff8938c129c322d6328dd183023095 Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Thu, 1 Oct 2026 02:55:22 +0200 Subject: [PATCH 037/100] Show the thinking level an agent runs on in its composer (#5804) The composer picked the thinking level from what was chosen at launch and fell back to the model's catalog default when nothing was chosen. A Pi agent started without a thinking level runs on Pi's own configured default, so the composer showed Medium while the agent ran on High. The composer now prefers the level the running agent reports when it is one of the model's options, the same way it already prefers the running model. Fixes #5800 Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> --- .../app/src/composer/agent-controls/index.tsx | 3 + .../src/composer/agent-controls/utils.test.ts | 55 +++++++++++++++++++ .../app/src/composer/agent-controls/utils.ts | 22 +++++++- 3 files changed, 78 insertions(+), 2 deletions(-) diff --git a/packages/app/src/composer/agent-controls/index.tsx b/packages/app/src/composer/agent-controls/index.tsx index 8dadeec217..d5f09dc77f 100644 --- a/packages/app/src/composer/agent-controls/index.tsx +++ b/packages/app/src/composer/agent-controls/index.tsx @@ -394,6 +394,7 @@ type AgentControlsSlice = { runtimeModelId: string | null; model: string | null | undefined; features: AgentFeature[] | undefined; + runtimeThinkingOptionId: string | null; thinkingOptionId: string | null | undefined; lastUsage: unknown; } | null; @@ -413,6 +414,7 @@ function selectAgentControlsSlice( runtimeModelId: currentAgent.runtimeInfo?.model ?? null, model: currentAgent.model, features: currentAgent.features, + runtimeThinkingOptionId: currentAgent.runtimeInfo?.thinkingOptionId ?? null, thinkingOptionId: currentAgent.thinkingOptionId, lastUsage: currentAgent.lastUsage, }; @@ -1594,6 +1596,7 @@ export const AgentControls = memo(function AgentControls({ models, runtimeModelId: agent?.runtimeModelId, configuredModelId: agent?.model, + runtimeThinkingOptionId: agent?.runtimeThinkingOptionId, explicitThinkingOptionId: agent?.thinkingOptionId, }); diff --git a/packages/app/src/composer/agent-controls/utils.test.ts b/packages/app/src/composer/agent-controls/utils.test.ts index 993e21f0c9..1c91d8ce79 100644 --- a/packages/app/src/composer/agent-controls/utils.test.ts +++ b/packages/app/src/composer/agent-controls/utils.test.ts @@ -67,6 +67,7 @@ describe("resolveAgentModelSelection", () => { ], runtimeModelId: null, configuredModelId: "claude-fable-5[1m]", + runtimeThinkingOptionId: null, explicitThinkingOptionId: null, }); @@ -88,6 +89,7 @@ describe("resolveAgentModelSelection", () => { ], runtimeModelId: "a", configuredModelId: "b", + runtimeThinkingOptionId: null, explicitThinkingOptionId: null, }); @@ -112,6 +114,7 @@ describe("resolveAgentModelSelection", () => { ], runtimeModelId: "a", configuredModelId: null, + runtimeThinkingOptionId: null, explicitThinkingOptionId: "high", }); @@ -119,6 +122,54 @@ describe("resolveAgentModelSelection", () => { expect(selection.displayThinking).toBe("High"); }); + it("shows the thinking level the agent runs on when none was chosen", () => { + const selection = resolveAgentModelSelection({ + models: [ + { + id: "openrouter/google/gemini-3.6-flash", + provider: "pi", + label: "Gemini 3.6 Flash", + thinkingOptions: [ + { id: "low", label: "Low" }, + { id: "medium", label: "Medium", isDefault: true }, + { id: "high", label: "High" }, + ], + defaultThinkingOptionId: "medium", + }, + ], + runtimeModelId: "openrouter/google/gemini-3.6-flash", + configuredModelId: null, + runtimeThinkingOptionId: "high", + explicitThinkingOptionId: null, + }); + + expect(selection.selectedThinkingId).toBe("high"); + expect(selection.displayThinking).toBe("High"); + }); + + it("falls back to the chosen thinking level when the runtime level is not a model option", () => { + const selection = resolveAgentModelSelection({ + models: [ + { + id: "a", + provider: "codex", + label: "Model A", + thinkingOptions: [ + { id: "low", label: "Low" }, + { id: "high", label: "High" }, + ], + defaultThinkingOptionId: "low", + }, + ], + runtimeModelId: "a", + configuredModelId: null, + runtimeThinkingOptionId: "xhigh", + explicitThinkingOptionId: "high", + }); + + expect(selection.selectedThinkingId).toBe("high"); + }); + it("formats raw thinking labels in the selected model display", () => { const selection = resolveAgentModelSelection({ models: [ @@ -134,6 +185,7 @@ describe("resolveAgentModelSelection", () => { ], runtimeModelId: "a", configuredModelId: null, + runtimeThinkingOptionId: null, explicitThinkingOptionId: "xhigh", }); @@ -155,6 +207,7 @@ describe("resolveAgentModelSelection", () => { ], runtimeModelId: null, configuredModelId: null, + runtimeThinkingOptionId: null, explicitThinkingOptionId: null, }); @@ -178,6 +231,7 @@ describe("resolveAgentModelSelection", () => { ], runtimeModelId: "claude-sonnet-4-6-20260101", configuredModelId: "default", + runtimeThinkingOptionId: null, explicitThinkingOptionId: null, }); @@ -204,6 +258,7 @@ describe("resolveAgentModelSelection", () => { ], runtimeModelId: "gpt-6.1-sol", configuredModelId: "gpt-6.1-sol", + runtimeThinkingOptionId: null, explicitThinkingOptionId: "max", }); diff --git a/packages/app/src/composer/agent-controls/utils.ts b/packages/app/src/composer/agent-controls/utils.ts index 56f9bcd49f..631b55753e 100644 --- a/packages/app/src/composer/agent-controls/utils.ts +++ b/packages/app/src/composer/agent-controls/utils.ts @@ -86,9 +86,16 @@ function pickSelectedModel( } function resolveThinkingId( + runtimeThinkingOptionId: string | null | undefined, explicitThinkingOptionId: string | null | undefined, selectedModel: AgentModelDefinition | null, ): string | null { + const runtimeThinkingOption = selectedModel?.thinkingOptions?.find( + (option) => option.id === runtimeThinkingOptionId, + ); + if (runtimeThinkingOption) { + return runtimeThinkingOption.id; + } if (explicitThinkingOptionId && explicitThinkingOptionId !== "default") { return explicitThinkingOptionId; } @@ -139,9 +146,16 @@ export function resolveAgentModelSelection(input: { models: AgentModelDefinition[] | null; runtimeModelId: string | null | undefined; configuredModelId: string | null | undefined; + runtimeThinkingOptionId: string | null | undefined; explicitThinkingOptionId: string | null | undefined; }) { - const { models, runtimeModelId, configuredModelId, explicitThinkingOptionId } = input; + const { + models, + runtimeModelId, + configuredModelId, + runtimeThinkingOptionId, + explicitThinkingOptionId, + } = input; const normalizedRuntimeModelId = normalizeModelId(runtimeModelId); const normalizedConfiguredModelId = normalizeModelId(configuredModelId); @@ -162,7 +176,11 @@ export function resolveAgentModelSelection(input: { ); const thinkingOptions = selectedModel?.thinkingOptions ?? null; - const resolvedThinkingId = resolveThinkingId(explicitThinkingOptionId, selectedModel); + const resolvedThinkingId = resolveThinkingId( + runtimeThinkingOptionId, + explicitThinkingOptionId, + selectedModel, + ); const effectiveThinking = resolveEffectiveThinking(thinkingOptions, resolvedThinkingId); const selectedThinkingId = effectiveThinking?.id ?? resolvedThinkingId; const displayThinking = resolveThinkingDisplay( From b701cb2dcd732cf79f953289d01fd3269ccc59a5 Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Thu, 1 Oct 2026 03:57:56 +0200 Subject: [PATCH 038/100] Show failed Codex image generations in the chat (#5810) When a Codex image request fails, Codex completes the imageGeneration item with status "failed", an empty result, and an optional failure record. The image mapper only knew how to render a successful image, so it returned nothing and the item vanished from the live timeline and from reopened history. Failed items now map to a failed image_generation tool call carrying the failure details. Closes #5619 Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> Co-authored-by: 3ae3ae <83870598+3ae3ae@users.noreply.github.com> --- .../providers/codex-app-server-agent.test.ts | 101 ++++++++++++++++++ .../agent/providers/codex-app-server-agent.ts | 9 ++ 2 files changed, 110 insertions(+) diff --git a/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts b/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts index fb27b68e96..032eeb65cb 100644 --- a/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts +++ b/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts @@ -6192,6 +6192,107 @@ describe("Codex app-server provider", () => { ]); }); + test.each([ + [ + "a usage limit", + { type: "usageLimitExceeded", limitId: "image_gen", resetsAt: 1786150800 }, + { type: "usageLimitExceeded", limitId: "image_gen", resetsAt: 1786150800 }, + ], + ["no failure details", null, { message: "Image generation failed" }], + ])( + "emits failed imageGeneration items with %s as failed tool calls", + (_label, failure, expectedError) => { + const session = createSession(); + const events: AgentStreamEvent[] = []; + session.subscribe((event) => events.push(event)); + + asInternals(session).handleNotification("item/completed", { + item: { + id: "image-generation-failed", + type: "imageGeneration", + status: "failed", + revisedPrompt: "paint a blue whale", + result: "", + transparentBackground: null, + failure, + savedPath: null, + }, + }); + + expect(events).toEqual([ + { + type: "timeline", + provider: "codex", + turnId: "test-turn", + item: { + type: "tool_call", + callId: "image-generation-failed", + name: "image_generation", + status: "failed", + error: expectedError, + detail: { + type: "unknown", + input: { prompt: "paint a blue whale" }, + output: null, + }, + }, + }, + ]); + }, + ); + + test("keeps failed imageGeneration items in persisted history", async () => { + const session = createSession(); + session.client = { + request: vi.fn(async () => ({ + thread: { + turns: [ + { + items: [ + { + type: "imageGeneration", + id: "image-generation-failed", + status: "failed", + revisedPrompt: "paint a blue whale", + result: "", + transparentBackground: null, + failure: { type: "usageLimitExceeded", limitId: "image_gen", resetsAt: null }, + savedPath: null, + }, + ], + }, + ], + }, + })), + }; + + await asInternals(session).loadPersistedHistory(session.client); + + const history: AgentStreamEvent[] = []; + for await (const event of session.streamHistory()) { + history.push(event); + } + + expect(history).toEqual([ + { + type: "timeline", + provider: "codex", + item: { + type: "tool_call", + callId: "image-generation-failed", + name: "image_generation", + status: "failed", + error: { type: "usageLimitExceeded", limitId: "image_gen", resetsAt: null }, + detail: { + type: "unknown", + input: { prompt: "paint a blue whale" }, + output: null, + }, + }, + }, + ]); + }); + test("emits usage_updated on token usage updates and keeps usage on turn completion", () => { const session = createSession(); const events: AgentStreamEvent[] = []; diff --git a/packages/server/src/server/agent/providers/codex-app-server-agent.ts b/packages/server/src/server/agent/providers/codex-app-server-agent.ts index f15c5a60ad..019a03bc59 100644 --- a/packages/server/src/server/agent/providers/codex-app-server-agent.ts +++ b/packages/server/src/server/agent/providers/codex-app-server-agent.ts @@ -1949,6 +1949,15 @@ function mapCodexThreadImageItem( }); } + if (normalizedItem.status === "failed") { + return mapCodexToolCallEnvelope({ + callId: firstStringField(normalizedItem, ["id"]), + name: "image_generation", + input: { prompt: firstStringField(normalizedItem, ["revisedPrompt", "revised_prompt"]) }, + error: normalizedItem.failure ?? { message: "Image generation failed" }, + }); + } + const savedPath = firstStringField(normalizedItem, ["savedPath", "saved_path"]); const result = codexImageOutputFromResult(normalizedItem.result); return renderProviderImageOutputAsAssistantMarkdown( From 15f14314160722c8019f4d1ec65ec4a2844faff6 Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Thu, 1 Oct 2026 05:01:45 +0200 Subject: [PATCH 039/100] Let bundlers resolve the published @getpaseo/relay package (#5815) The relay exports map sent the import and default conditions to src/*.ts, which the published package does not include, so any bundler resolving @getpaseo/client failed with 'Could not resolve "@getpaseo/relay/e2ee"'. Every condition now resolves to dist, like @getpaseo/protocol and @getpaseo/client. The client build now builds relay first, and the Metro workaround for compiling relay source is removed. Fixes #4730 Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> --- packages/app/metro.config.cjs | 11 ----- packages/client/package.json | 2 +- packages/relay/package.json | 12 ++---- packages/relay/src/package-exports.test.ts | 49 ++++++++++++++++++++++ 4 files changed, 53 insertions(+), 21 deletions(-) create mode 100644 packages/relay/src/package-exports.test.ts diff --git a/packages/app/metro.config.cjs b/packages/app/metro.config.cjs index e26fa04c32..c1fdbdef77 100644 --- a/packages/app/metro.config.cjs +++ b/packages/app/metro.config.cjs @@ -1,12 +1,10 @@ const { getDefaultConfig } = require("expo/metro-config"); const { resolve } = require("metro-resolver"); -const fs = require("fs"); const path = require("path"); const projectRoot = __dirname; const appNodeModulesRoot = path.resolve(projectRoot, "node_modules"); const appSrcRoot = path.resolve(projectRoot, "src"); -const relaySrcRoot = path.resolve(projectRoot, "../relay/src"); const isFdroidBuild = process.env.PASEO_FDROID_BUILD === "1"; const fdroidModuleOverrides = { "expo-camera": path.resolve(appSrcRoot, "fdroid/expo-camera.tsx"), @@ -81,15 +79,6 @@ config.resolver.resolveRequest = (context, moduleName, platform) => { return resolveWithCustomWebOverlay(context, fdroidModuleOverrides[moduleName], platform); } - const origin = context.originModulePath; - if (origin && origin.startsWith(relaySrcRoot) && moduleName.endsWith(".js")) { - const tsModuleName = moduleName.replace(/\.js$/, ".ts"); - const candidatePath = path.resolve(path.dirname(origin), tsModuleName); - if (fs.existsSync(candidatePath)) { - return resolveWithCustomWebOverlay(context, tsModuleName, platform); - } - } - return resolveWithCustomWebOverlay(context, moduleName, platform); }; diff --git a/packages/client/package.json b/packages/client/package.json index 44f3780b56..4e5a9124c8 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -31,7 +31,7 @@ }, "scripts": { "clean": "node ../../scripts/clean-package-dist.mjs", - "build": "npm run build --workspace=@getpaseo/protocol && tsc -p tsconfig.json --incremental false", + "build": "npm run build --workspace=@getpaseo/protocol && npm run build --workspace=@getpaseo/relay && tsc -p tsconfig.json --incremental false", "build:clean": "npm run clean && npm run build", "prepack": "npm run build:clean", "typecheck": "tsgo --noEmit", diff --git a/packages/relay/package.json b/packages/relay/package.json index 3693a832f6..10185476e3 100644 --- a/packages/relay/package.json +++ b/packages/relay/package.json @@ -11,21 +11,15 @@ "exports": { ".": { "types": "./dist/index.d.ts", - "node": "./dist/index.js", - "import": "./src/index.ts", - "default": "./src/index.ts" + "default": "./dist/index.js" }, "./e2ee": { "types": "./dist/e2ee.d.ts", - "node": "./dist/e2ee.js", - "import": "./src/e2ee.ts", - "default": "./src/e2ee.ts" + "default": "./dist/e2ee.js" }, "./cloudflare": { "types": "./dist/cloudflare-adapter.d.ts", - "node": "./dist/cloudflare-adapter.js", - "import": "./src/cloudflare-adapter.ts", - "default": "./src/cloudflare-adapter.ts" + "default": "./dist/cloudflare-adapter.js" } }, "publishConfig": { diff --git a/packages/relay/src/package-exports.test.ts b/packages/relay/src/package-exports.test.ts new file mode 100644 index 0000000000..2e8dda9395 --- /dev/null +++ b/packages/relay/src/package-exports.test.ts @@ -0,0 +1,49 @@ +import { existsSync, readFileSync } from "node:fs"; +import { execFileSync } from "node:child_process"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { describe, expect, it } from "vitest"; + +const THIS_DIR = path.dirname(fileURLToPath(import.meta.url)); +const RELAY_PACKAGE_ROOT = path.resolve(THIS_DIR, ".."); + +interface PackageManifest { + exports: Record>; +} + +function readManifest(): PackageManifest { + return JSON.parse(readFileSync(path.join(RELAY_PACKAGE_ROOT, "package.json"), "utf8")); +} + +function listPublishedFiles(): Set { + if (!existsSync(path.join(RELAY_PACKAGE_ROOT, "dist/index.js"))) { + execFileSync("npm", ["run", "build"], { cwd: RELAY_PACKAGE_ROOT, stdio: "inherit" }); + } + const output = execFileSync("npm", ["pack", "--dry-run", "--json", "--ignore-scripts"], { + cwd: RELAY_PACKAGE_ROOT, + encoding: "utf8", + }); + const [pack] = JSON.parse(output) as Array<{ files: Array<{ path: string }> }>; + return new Set(pack.files.map((file) => file.path)); +} + +function listExportTargets(manifest: PackageManifest): Array<{ label: string; file: string }> { + const targets: Array<{ label: string; file: string }> = []; + for (const [subpath, conditions] of Object.entries(manifest.exports)) { + for (const [condition, target] of Object.entries(conditions)) { + targets.push({ label: `${subpath} [${condition}] -> ${target}`, file: target.slice(2) }); + } + } + return targets; +} + +describe("published relay package", () => { + it("ships every file its exports map resolves to, for every condition", () => { + const published = listPublishedFiles(); + const missing = listExportTargets(readManifest()) + .filter((target) => !published.has(target.file)) + .map((target) => target.label); + + expect(missing).toEqual([]); + }); +}); From e939dd4c7a5cb3f6da73006dc3eee9c90bcbdf3d Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Thu, 1 Oct 2026 04:55:17 +0100 Subject: [PATCH 040/100] Streamline sidebar usage and stacked sheet interactions (#5805) * feat(app): streamline sidebar usage and stacked sheets * fix(app): restore compact sidebar navigation clicks The close button container emitted box-none through Unistyles as invalid CSS and intercepted New workspace clicks. Let React Native apply click-through behavior at the view boundary. Keep the import journey on its existing New workspace screen and assert only the visible transcript. * fix(app): cap menu sheets at eighty percent height Keep long sidebar usage reports recognizably sheet-sized while allowing the content to scroll. Short menu sheets retain dynamic sizing. --- docs/floating-panels.md | 16 ++ docs/glossary.md | 2 +- docs/menus.md | 2 + docs/plugins.md | 2 +- .../e2e/browser/bottom-sheet-reopen.spec.ts | 14 ++ .../e2e/browser/import-session-flow.spec.ts | 9 +- .../e2e/browser/sidebar-nav-settings.spec.ts | 4 +- packages/app/e2e/browser/usage-screen.spec.ts | 6 +- .../app/e2e/browser/usage-settings.spec.ts | 5 +- .../e2e/browser/usage-sidebar-item.spec.ts | 110 +++++++-- .../app/e2e/support/helpers/import-session.ts | 33 ++- .../app/e2e/support/helpers/new-workspace.ts | 3 +- .../support/helpers/sidebar-nav-settings.ts | 38 ++-- .../app/e2e/support/helpers/startup-dsl.ts | 1 - .../e2e/support/helpers/usage-sidebar-item.ts | 112 +++++++++- .../app/src/command-center/command-center.tsx | 14 +- .../src/components/adaptive-modal-sheet.tsx | 10 +- packages/app/src/components/left-sidebar.tsx | 178 ++++----------- .../components/sidebar/sidebar-header-row.tsx | 13 +- .../app/src/components/tool-call-sheet.tsx | 11 +- packages/app/src/components/ui/combobox.tsx | 20 +- .../ui/isolated-bottom-sheet-modal/index.tsx | 42 +++- .../sheet-backdrop.tsx | 5 + .../sheet-backdrop.web.tsx | 36 +++ .../src/components/ui/menu/menu-surface.tsx | 21 +- packages/app/src/i18n/resources/ar.ts | 1 + packages/app/src/i18n/resources/en.ts | 1 + packages/app/src/i18n/resources/es.ts | 1 + packages/app/src/i18n/resources/fr.ts | 1 + packages/app/src/i18n/resources/ja.ts | 1 + packages/app/src/i18n/resources/ko.ts | 1 + packages/app/src/i18n/resources/pt-BR.ts | 1 + packages/app/src/i18n/resources/ru.ts | 1 + packages/app/src/i18n/resources/zh-CN.ts | 1 + .../app/src/screens/new-workspace-screen.tsx | 24 +- .../new-workspace/import-session-button.tsx | 50 +++++ packages/app/src/sidebar-nav/model.test.ts | 42 +++- packages/app/src/sidebar-nav/model.ts | 21 +- .../src/sidebar-nav/use-sidebar-nav-items.ts | 12 +- .../detail-level/overview/sheet.tsx | 15 +- packages/app/src/usage/balance-bar.tsx | 15 +- packages/app/src/usage/controls.tsx | 36 +-- packages/app/src/usage/copy.ts | 3 + packages/app/src/usage/display-toggle.tsx | 22 -- packages/app/src/usage/format.test.ts | 15 ++ packages/app/src/usage/format.ts | 7 +- packages/app/src/usage/index.ts | 2 +- packages/app/src/usage/meter.tsx | 74 ++++++ packages/app/src/usage/options-menu.tsx | 103 +++++++++ packages/app/src/usage/pinned.test.ts | 139 +++++++++--- packages/app/src/usage/pinned.ts | 112 +++++++--- packages/app/src/usage/sidebar-item.tsx | 210 +++++++++++++++--- packages/app/src/usage/tone.ts | 8 +- packages/app/src/usage/window-bar.tsx | 64 +----- .../app/test-stubs/gorhom-bottom-sheet.ts | 4 + packages/plugin/src/server/usage.ts | 11 + packages/protocol/src/messages.ts | 7 + .../claude-usage-source/server/usage.test.ts | 32 +++ plugins/claude-usage-source/server/usage.ts | 9 +- .../codex-usage-source/server/usage.test.ts | 26 ++- plugins/codex-usage-source/server/usage.ts | 23 +- plugins/grok-usage-source/server/usage.ts | 1 + plugins/muse-provider/server/usage.ts | 4 + .../server/usage.test.ts | 2 + .../opencode-go-usage-source/server/usage.ts | 10 +- 65 files changed, 1292 insertions(+), 527 deletions(-) create mode 100644 packages/app/src/components/ui/isolated-bottom-sheet-modal/sheet-backdrop.tsx create mode 100644 packages/app/src/components/ui/isolated-bottom-sheet-modal/sheet-backdrop.web.tsx create mode 100644 packages/app/src/screens/new-workspace/import-session-button.tsx delete mode 100644 packages/app/src/usage/display-toggle.tsx create mode 100644 packages/app/src/usage/format.test.ts create mode 100644 packages/app/src/usage/meter.tsx create mode 100644 packages/app/src/usage/options-menu.tsx diff --git a/docs/floating-panels.md b/docs/floating-panels.md index b0c16b3e4a..9c4e793fb8 100644 --- a/docs/floating-panels.md +++ b/docs/floating-panels.md @@ -302,6 +302,22 @@ Do not treat `onChange(-1)` as a close by itself. In a stacked another pushed sheet. Close React state from `onDismiss`; use `onChange` only to track phase. +All sheets use the app-wide Gorhom modal provider through `IsolatedBottomSheetModal`. This +includes native centered dialogs: separate React Native Modals cannot present a root-owned +sibling dialog over an existing iOS presenter. + +The wrapper must give Gorhom an object ref. Gorhom accepts a callback ref at the React boundary +but stores it in the modal queue and later reads `.current`; provider dismissal, replacement, +and restoration then silently do nothing. Callers may use callback refs on the wrapper, which +forwards commands to the current Gorhom handle. + +Request a backdrop with `backdropOpacity`; do not install a caller-owned backdrop. On web, +Gorhom's default backdrop closes its own sheet, even when another modal is above it. Its animated +pointer-event changes can expose a lower backdrop during a transition. The shared web backdrop +keeps its press target until portal teardown and asks the provider to dismiss the top modal. +Gorhom's presentation queue remains the authority; backdrop mount order is irrelevant. Native +retains Gorhom's gesture backdrop and the same push/replace presentation semantics. + ## Gotcha 7 — A sheet cannot read context from its call site React cannot copy contexts reflectively, so the only way across the teleport in diff --git a/docs/glossary.md b/docs/glossary.md index 03b65db022..512068ece8 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -19,7 +19,7 @@ Authoritative terminology. UI label wins. Don't invent synonyms; use what's here - **MR** — GitLab merge request. UI label for GitLab change requests only; do not use MR for GitHub/Gitea/Forgejo. - **Worktree** — Paseo-managed git worktree (`~/.paseo/worktrees/{name}`); also a `workspaceKind` value. User-facing creation treats it as the `worktree` workspace isolation choice. Code and `paseo.json` retain worktree terminology for git lifecycle implementation. Forbidden: "Checkout" as a product synonym. - **Repository / Remote** — Internal Git observations. They may produce mutable kind, branch, and project-grouping metadata but never the host-local project ID, root, display name, or workspace membership. No UI label. -- **Sidebar items** — The configurable entries of the app sidebar, in two sections. Header: the rows at the head of the sidebar — New workspace, History, Search, Schedules, and plugin header items. Footer: the full-width rows between Add project and the footer icon row — Usage and plugin footer items. Add project and the icon row (Hosts, Import session, Help and support, Settings) are fixed and are not sidebar items. A plugin item may render several rows, such as one per bot; it is still one item that moves and hides as a block. Each section's order and visibility is one per-device preference (`AppSettings.sidebarNavItems`, `AppSettings.sidebarFooterItems`); every item may be off. UI: the Header and Footer cards on the Sidebar settings page. Code: `packages/app/src/sidebar-nav/model.ts` resolves a section, and the sidebar (`components/sidebar/sidebar-nav-rows.tsx`, the footer in `components/left-sidebar.tsx`) and the settings page (`screens/settings/sidebar/sidebar-nav-section.tsx`) read it through `useSidebarNavItems(section)`. Never hardcode one of these entries or add a second place that decides which ones show. Don't confuse with **Explorer sidebar**. +- **Sidebar items** — The configurable entries of the app sidebar, in two sections. Header: the rows at the head of the sidebar — New workspace, History, Search, Schedules, and plugin header items. Footer: the full-width rows between Add project and the footer icon row — Usage and plugin footer items. Add project and the icon row (Hosts, Help and support, Settings) are fixed and are not sidebar items. A plugin item may render several rows, such as one per bot; it is still one item that moves and hides as a block. Each section's order and visibility is one per-device preference (`AppSettings.sidebarNavItems`, `AppSettings.sidebarFooterItems`); every item may be off. UI: the Header and Footer cards on the Sidebar settings page. Code: `packages/app/src/sidebar-nav/model.ts` resolves a section, and the sidebar (`components/sidebar/sidebar-nav-rows.tsx`, the footer in `components/left-sidebar.tsx`) and the settings page (`screens/settings/sidebar/sidebar-nav-section.tsx`) read it through `useSidebarNavItems(section)`. Never hardcode one of these entries or add a second place that decides which ones show. Don't confuse with **Explorer sidebar**. - **Explorer sidebar** — The dedicated navigation dock containing the Files and Changes trees. UI: "Explorer sidebar". Code: `packages/app/src/workspace-tabs/explorer-sidebar.ts`; persisted pane identifiers retain the `explorer` spelling. See [explorer-sidebar.md](explorer-sidebar.md). - **Side pane** — An ordinary right-side workspace pane used for content opened beside the user's work. It closes and participates in workspace splits like every other main-host pane. See [explorer-sidebar.md](explorer-sidebar.md#side-pane). Forbidden: "Side panel". - **Directory-backed surface** — A right-sidebar surface whose content is determined by the workspace's `cwd`, so two workspaces on the same directory see identical content: git diff/status, forge change-request info, file preview/explorer contents. Keyed by `(serverId, cwd)`, never `workspaceId`. See [architecture.md](architecture.md#right-sidebar-boundary-directory-backed-vs-workspace-owned). diff --git a/docs/menus.md b/docs/menus.md index 3a6829d738..6791d28e71 100644 --- a/docs/menus.md +++ b/docs/menus.md @@ -191,6 +191,8 @@ its own. [floating-panels.md](floating-panels.md). - **One overlay per menu.** Submenus render inside their parent's layer and paint no second backdrop, so there is exactly one `Modal` on native no matter how deep the menu goes. +- **Sheet stacking and backdrop dismissal belong to `IsolatedBottomSheetModal`.** Menus only + request a backdrop opacity. See [floating-panels.md](floating-panels.md#gotcha-6--bottom-sheet-refs-are-not-lifecycle-truth). - **Retained panels own visibility.** The shared menu surface unmounts when its panel becomes inactive. An async action can navigate before its menu closes; a hidden panel must not leave a portal backdrop blocking the destination. On web the chat suspends one commit after it goes diff --git a/docs/plugins.md b/docs/plugins.md index 91c0841ffe..b4471d10e7 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -478,7 +478,7 @@ one section's order from built-ins, plugin groups, and the section's preference - The item's `Component` renders directly in the section, with no wrapper, so a fragment or array of rows lays out like separate items while Settings keeps one entry for the block. Footer items - are rows between Add project and the footer's icon row. The icon row (Hosts, Import session, + are rows between Add project and the footer's icon row. The icon row (Hosts, Help and support, Settings) is fixed app code, not a contribution slot, so the kit has no icon button. - `openPopover` opens through the same `PluginPopoverSurface` as header buttons diff --git a/packages/app/e2e/browser/bottom-sheet-reopen.spec.ts b/packages/app/e2e/browser/bottom-sheet-reopen.spec.ts index 967462f05b..89ee10af22 100644 --- a/packages/app/e2e/browser/bottom-sheet-reopen.spec.ts +++ b/packages/app/e2e/browser/bottom-sheet-reopen.spec.ts @@ -84,6 +84,18 @@ async function openModelSelector(page: Page) { await expect(page.getByTestId("agent-controls-settings-list")).toBeVisible({ timeout: 10_000 }); } +async function dismissStackedModelPickerWithBackdrop(page: Page) { + await page + .getByTestId("agent-controls-settings-list") + .getByRole("button", { name: /Select model/ }) + .click(); + await expect(page.getByTestId("agent-controls-model-browser-sheet")).toBeVisible(); + await page.mouse.click(MOBILE_VIEWPORT.width / 2, 24); + await expect(page.getByTestId("agent-controls-model-browser-sheet")).not.toBeVisible(); + await expect(page.getByTestId("agent-controls-model-sheet")).toBeVisible(); + await expect(page.getByTestId("agent-controls-settings-list")).toBeVisible(); +} + async function openAndCloseTabSwitcherTwice(page: Page) { await openTabSwitcher(page); await closeBottomSheetWithBackdrop(page); @@ -114,6 +126,8 @@ test.describe("mobile bottom sheet reopen", () => { await openModelSelector(page); const sheet = page.getByTestId("agent-controls-model-sheet"); + await dismissStackedModelPickerWithBackdrop(page); + await page.getByTestId("model-search-all-input").click(); const model = page.getByRole("button", { name: /^Ten second stream/ }); await expect(model).toBeVisible({ diff --git a/packages/app/e2e/browser/import-session-flow.spec.ts b/packages/app/e2e/browser/import-session-flow.spec.ts index 592d31c330..788a6f93ec 100644 --- a/packages/app/e2e/browser/import-session-flow.spec.ts +++ b/packages/app/e2e/browser/import-session-flow.spec.ts @@ -128,7 +128,6 @@ test("an open Import Session row keeps its age current", async ({ page }) => { await page.clock.install({ time: scenario.importSessionTimestamp + 60_000 }); const flow = new ImportSessionFlow(page); await flow.openWorkspace(scenario.project.workspaceId, { width: 390, height: 844 }); - await flow.revealMobileEntryPoint(); await flow.openGlobally(); const row = page.getByTestId(`import-session-session-claude-${scenario.importSessionId}`); @@ -141,13 +140,13 @@ test("captures the compact import-session journey", async ({ page }, testInfo) = const flow = new ImportSessionFlow(page); await flow.openWorkspace(scenario.project.workspaceId, { width: 390, height: 844 }); - await test.step("the mobile sidebar exposes import in its footer", async () => { - await flow.revealMobileEntryPoint(); - await capture(page, testInfo, "01-mobile-sidebar-footer.png"); + await test.step("the new workspace screen offers import at the top", async () => { + await flow.revealNewWorkspaceEntryPoint(); + await capture(page, testInfo, "01-mobile-new-workspace-import.png"); }); await test.step("the host-wide sheet is newest first and fits its provider filter", async () => { - await flow.openGlobally(); + await flow.openFromNewWorkspace(); await flow.expectScope("Sessions on", false); await flow.expectRows({ first: [scenario.importSessionId, "fixture-worktree", "fixture-unrelated"], diff --git a/packages/app/e2e/browser/sidebar-nav-settings.spec.ts b/packages/app/e2e/browser/sidebar-nav-settings.spec.ts index 038ea63f95..ae7919e952 100644 --- a/packages/app/e2e/browser/sidebar-nav-settings.spec.ts +++ b/packages/app/e2e/browser/sidebar-nav-settings.spec.ts @@ -31,13 +31,13 @@ import { setSidebarNavItemVisible, } from "../support/helpers/sidebar-nav-settings"; -test("fixed footer line keeps the labeled Add project button and four icons", async ({ page }) => { +test("fixed footer line keeps its five icons, Help and Settings at the end", async ({ page }) => { test.setTimeout(120_000); await installUsageReportsFixture(page, { lists: [() => claudeAndCodexReports()] }); await seedSidebarFooterPreferences(page, [{ key: "add-project", visible: false }]); await page.setViewportSize({ width: 1440, height: 900 }); await gotoAppShell(page); - await expectPinnedUsage(page, ["31%", "7%"]); + await expectPinnedUsage(page, ["31% 5h", "54% wk", "7% 5h", "12% wk"]); await expectFooterIconRow(page); await expectFooterSeparator(page, true); await footerScreenshot(page, "footer-desktop-with-rows"); diff --git a/packages/app/e2e/browser/usage-screen.spec.ts b/packages/app/e2e/browser/usage-screen.spec.ts index 78deb7835f..c7de5261aa 100644 --- a/packages/app/e2e/browser/usage-screen.spec.ts +++ b/packages/app/e2e/browser/usage-screen.spec.ts @@ -215,7 +215,7 @@ test.describe("usage screen", () => { }); // Nothing picked and no workspace open: the first host. - await expectPinnedUsage(page, ["31%"]); + await expectPinnedUsage(page, ["31% Weekly"]); await usageItem(page).click(); await expect(page.getByTestId(`usage-host-${primaryServerId}`)).toBeVisible(); @@ -226,7 +226,7 @@ test.describe("usage screen", () => { ).toBeVisible({ timeout: 30_000 }); await expect(hostFilter(page)).toContainText("Secondary box"); await qaScreenshot(page, "usage-screen-picked-host"); - await expectPinnedUsage(page, ["12%"]); + await expectPinnedUsage(page, ["12% Weekly"]); // The e2e seed resets the host list on every load, so reopening re-adds the second host. await addConnectedHostAndReload(page, { @@ -234,7 +234,7 @@ test.describe("usage screen", () => { label: "Secondary box", port: secondary.port, }); - await expectPinnedUsage(page, ["12%"]); + await expectPinnedUsage(page, ["12% Weekly"]); await expect( page.getByTestId(`usage-host-${secondary.serverId}`).getByText("12%"), ).toBeVisible({ timeout: 30_000 }); diff --git a/packages/app/e2e/browser/usage-settings.spec.ts b/packages/app/e2e/browser/usage-settings.spec.ts index 1c590c38be..5ad817aeb1 100644 --- a/packages/app/e2e/browser/usage-settings.spec.ts +++ b/packages/app/e2e/browser/usage-settings.spec.ts @@ -7,6 +7,7 @@ import { installUsageReportsFixture, type UsageReportsFixture, } from "../support/helpers/usage-reports"; +import { refreshAllUsage, showUsageAs } from "../support/helpers/usage-sidebar-item"; const ICON = ''; @@ -89,7 +90,7 @@ test.describe("usage settings", () => { await expect(card.getByText("2026-12-31", { exact: true })).toBeVisible(); await expect(card.getByText("Gamma auth expired", { exact: true })).toBeVisible(); - await card.getByTestId("usage-display-remaining").click(); + await showUsageAs(page, "remaining"); await expect(card.getByText("30% left")).toBeVisible(); await expect(card.getByText("93% left")).toBeVisible(); }); @@ -117,7 +118,7 @@ test.describe("usage settings", () => { const card = page.getByTestId("usage-card"); await expect(card.getByText("23%")).toBeVisible({ timeout: 10_000 }); - await page.getByRole("button", { name: "Refresh", exact: true }).click(); + await refreshAllUsage(page); await expect.poll(() => forcedRefreshCount(usage)).toBe(1); await expect(card.getByText("64%")).toBeVisible(); }); diff --git a/packages/app/e2e/browser/usage-sidebar-item.spec.ts b/packages/app/e2e/browser/usage-sidebar-item.spec.ts index e09e4d22c6..124eaca5f6 100644 --- a/packages/app/e2e/browser/usage-sidebar-item.spec.ts +++ b/packages/app/e2e/browser/usage-sidebar-item.spec.ts @@ -4,7 +4,11 @@ import { expect, test } from "../support/fixtures"; import { gotoAppShell, openSettings } from "../support/helpers/app"; import { getServerId } from "../support/helpers/server-id"; import { openSettingsHostSection } from "../support/helpers/settings"; -import { leaveSettings, openSidebarNavSettings } from "../support/helpers/sidebar-nav-settings"; +import { + leaveSettings, + openSidebarNavSettings, + setFooterItemVisible, +} from "../support/helpers/sidebar-nav-settings"; import { installUsageReportsFixture } from "../support/helpers/usage-reports"; import { claudeAndCodexReports, @@ -13,11 +17,15 @@ import { leaveUsageScreen, openCompactSidebar, pinRow, + openUsageOptions, showUsageAs, togglePin, usageItem, usageSheet, + installTallUsageSource, } from "../support/helpers/usage-sidebar-item"; +import { seedSidebarFooterPreferences } from "../support/helpers/sidebar-nav-settings"; +import { waitForSettledPosition } from "../support/helpers/sheet-layout"; const WIDE = { width: 1440, height: 900 }; const COMPACT = { width: 390, height: 844 }; @@ -47,6 +55,32 @@ async function footerClip(page: Page) { } test.describe("Usage item", () => { + test("long usage reports stay in an 80% sheet and scroll to the final window", async ({ + page, + }) => { + test.setTimeout(120_000); + const source = await installTallUsageSource(); + try { + await page.setViewportSize(COMPACT); + await seedSidebarFooterPreferences(page, [{ key: "usage", visible: true }]); + await gotoAppShell(page); + await openCompactSidebar(page); + await expect(usageItem(page)).toBeInViewport(); + await usageItem(page).click(); + const content = page.getByTestId("sidebar-usage-sheet-content"); + await expect(content.getByText("Window 20", { exact: true })).toHaveCount(1); + await waitForSettledPosition(content); + const bounds = (await content.boundingBox())!; + expect(bounds.y).toBeGreaterThanOrEqual(COMPACT.height * 0.2); + await expect(content.getByText("Window 20", { exact: true })).not.toBeInViewport(); + await content.getByText("Window 20", { exact: true }).scrollIntoViewIfNeeded(); + await expect(content.getByText("Window 20", { exact: true })).toBeInViewport(); + await content.getByText("Window 1", { exact: true }).scrollIntoViewIfNeeded(); + await expect(content.getByText("Window 1", { exact: true })).toBeInViewport(); + } finally { + await source.cleanup(); + } + }); test("pinned windows show in the sidebar, follow the used/remaining toggle and persist", async ({ page, }) => { @@ -59,15 +93,20 @@ test.describe("Usage item", () => { await test.step("a fresh device shows default windows, which opens the Usage screen", async () => { await expect(usageItem(page)).toBeVisible({ timeout: 30_000 }); - await expectPinnedUsage(page, ["31%", "7%"]); + await expectPinnedUsage(page, ["31% 5h", "54% wk", "7% 5h", "12% wk"]); await qaScreenshot(page, "desktop-footer-defaults", { kind: "footer" }); await openSidebarNavSettings(page); await qaScreenshot(page, "desktop-settings-sidebar-footer"); await page.setViewportSize(COMPACT); + // Phones start with the Usage item off; turning it on stores the choice. + await expect( + page.getByTestId("sidebar-nav-section-footer").getByTestId("sidebar-nav-toggle-usage"), + ).toHaveAttribute("aria-checked", "false"); + await setFooterItemVisible(page, "usage", true); await qaScreenshot(page, "compact-settings-sidebar-footer"); await gotoAppShell(page); await openCompactSidebar(page); - await expectPinnedUsage(page, ["31%", "7%"]); + await expectPinnedUsage(page, ["31% 5h", "54% wk", "7% 5h", "12% wk"]); await qaScreenshot(page, "compact-footer-defaults"); await page.setViewportSize(WIDE); await gotoAppShell(page); @@ -82,12 +121,12 @@ test.describe("Usage item", () => { 0, ); await togglePin(screen, "Claude", "Session"); - await expectPinnedUsage(page, ["31%"]); + await expectPinnedUsage(page, ["31% 5h"]); await togglePin(screen, "Codex", "Weekly"); - await expectPinnedUsage(page, ["31%", "12%"]); + await expectPinnedUsage(page, ["31% 5h", "12% wk"]); await expect(usageItem(page)).not.toHaveText("Usage"); await qaScreenshot(page, "desktop-footer-pins", { kind: "footer" }); - await expect(page.getByRole("button", { name: "Refresh", exact: true })).toBeVisible(); + await expect(page.getByRole("button", { name: "Usage options", exact: true })).toBeVisible(); await qaScreenshot(page, "usage-screen-header", { kind: "element", locator: page.getByTestId("page-title").locator(".."), @@ -100,7 +139,7 @@ test.describe("Usage item", () => { await test.step("remaining flips the Usage item and the Usage screen", async () => { await showUsageAs(page, "remaining"); - await expectPinnedUsage(page, ["69%", "88%"]); + await expectPinnedUsage(page, ["69% 5h", "88% wk"]); await expect(usageItem(page)).toHaveAccessibleName(/Claude .*69% left, Codex .*88% left/); await expect( screen.getByTestId("usage-report-claude:default").getByText("69% left"), @@ -123,7 +162,7 @@ test.describe("Usage item", () => { await leaveUsageScreen(page); await openCompactSidebar(page); await expect(usageItem(page)).toBeInViewport(); - await expectPinnedUsage(page, ["69%", "88%"]); + await expectPinnedUsage(page, ["69% 5h", "88% wk"]); await qaScreenshot(page, "compact-footer"); await usageItem(page).click(); const sheet = usageSheet(page); @@ -136,7 +175,18 @@ test.describe("Usage item", () => { /^Pin Claude Session, \d+% left( · .+)?$/, ); // The sheet carries the Usage screen's controls. - await expect(page.locator('[data-testid="usage-refresh-all"]:visible')).toBeVisible(); + await expect(page.locator('[data-testid="usage-options-menu"]:visible')).toBeVisible(); + // The options open as a sheet of their own over the usage sheet. + await openUsageOptions(page); + await expect(page.getByTestId("usage-display-remaining")).toHaveAttribute( + "aria-checked", + "true", + ); + await qaScreenshot(page, "compact-options-sheet"); + // Tap the backdrop above both sheets; only the options sheet closes. + await page.mouse.click(COMPACT.width / 2, 40); + await expect(page.getByTestId("usage-display-used")).toHaveCount(0); + await expect(sheet).toBeVisible(); await expect(page).not.toHaveURL(/\/usage$/); const sheetBox = (await sheet.boundingBox())!; expect(sheetBox.y).toBeGreaterThan(COMPACT.height / 3); @@ -153,10 +203,14 @@ test.describe("Usage item", () => { await test.step("a reload keeps the pins and the toggle", async () => { await page.reload(); - await expectPinnedUsage(page, ["69%", "88%"]); - await expect( - page.locator('[data-testid="usage-display-remaining"]:visible').first(), - ).toHaveAttribute("aria-selected", "true"); + await expectPinnedUsage(page, ["69% 5h", "88% wk"]); + await openUsageOptions(page); + await expect(page.getByTestId("usage-display-remaining")).toHaveAttribute( + "aria-checked", + "true", + ); + await qaScreenshot(page, "usage-options-menu"); + await page.keyboard.press("Escape"); }); await test.step("the Settings usage section shares the pins and the toggle", async () => { @@ -173,10 +227,10 @@ test.describe("Usage item", () => { await test.step("unpinning both brings back default windows, and that survives a reload", async () => { await togglePin(screen, "Claude", "Session"); await togglePin(screen, "Codex", "Weekly"); - await expectPinnedUsage(page, ["69%", "93%"]); + await expectPinnedUsage(page, ["69% 5h", "46% wk", "93% 5h", "88% wk"]); await page.reload(); await expect(screen.getByText("88% left")).toBeVisible({ timeout: 10_000 }); - await expectPinnedUsage(page, ["69%", "93%"]); + await expectPinnedUsage(page, ["69% 5h", "46% wk", "93% 5h", "88% wk"]); await gotoAppShell(page); await usageItem(page).click(); await expectOnUsageScreen(page); @@ -194,6 +248,28 @@ test.describe("Usage item", () => { }); }); +test("the Usage options show and hide the sidebar Usage item", async ({ page }) => { + await installUsageReportsFixture(page, { lists: [() => claudeAndCodexReports()] }); + await page.setViewportSize(WIDE); + await gotoAppShell(page); + await expect(usageItem(page)).toBeVisible({ timeout: 30_000 }); + await usageItem(page).click(); + await expectOnUsageScreen(page); + + await openUsageOptions(page); + const toggle = page.getByTestId("usage-show-in-sidebar"); + await expect(toggle).toHaveAttribute("aria-checked", "true"); + await toggle.click(); + await expect(page.locator('[data-testid="sidebar-usage"]:visible')).toHaveCount(0); + + await page.reload(); + await expectOnUsageScreen(page); + await openUsageOptions(page); + await expect(toggle).toHaveAttribute("aria-checked", "false"); + await toggle.click(); + await expect(usageItem(page)).toBeVisible(); +}); + test("released hosts supply source logos through the client conversion", async ({ page }) => { await installUsageReportsFixture(page, { lists: [() => claudeAndCodexReports()], @@ -201,12 +277,12 @@ test("released hosts supply source logos through the client conversion", async ( }); await page.setViewportSize(WIDE); await gotoAppShell(page); - await expectPinnedUsage(page, ["31%", "7%"]); + await expectPinnedUsage(page, ["31% 5h", "54% wk", "7% 5h", "12% wk"]); // Source logos are decorative SVGs with no accessible role. Their path data distinguishes // the source artwork from the fallback gauge. const claudePath = /]* d="([^"]+)"/.exec(claudeAndCodexReports()[0]!.icon!)![1]!; const codexPath = /]* d="([^"]+)"/.exec(claudeAndCodexReports()[1]!.icon!)![1]!; - const summary = usageItem(page).getByTestId("sidebar-usage-pinned-window"); + const summary = usageItem(page).getByTestId("sidebar-usage-source"); await expect(summary.nth(0).locator("svg path").first()).toHaveAttribute("d", claudePath); await expect(summary.nth(1).locator("svg path").first()).toHaveAttribute("d", codexPath); await qaScreenshot(page, "released-host-footer", { kind: "footer" }); diff --git a/packages/app/e2e/support/helpers/import-session.ts b/packages/app/e2e/support/helpers/import-session.ts index 0bf2fc16e9..718b0d5965 100644 --- a/packages/app/e2e/support/helpers/import-session.ts +++ b/packages/app/e2e/support/helpers/import-session.ts @@ -2,6 +2,7 @@ import { expect, type Page } from "@playwright/test"; import { buildHostWorkspaceRoute } from "@/utils/host-routes"; import { gotoAppShell } from "./app"; import { openCommandCenter } from "./command-center"; +import { openGlobalNewWorkspaceComposer } from "./new-workspace"; import { getServerId } from "./server-id"; import { expectMobileAgentSidebarVisible, openMobileAgentSidebar } from "./sidebar"; @@ -18,16 +19,24 @@ export class ImportSessionFlow { timeout: 30_000, }); } - async revealMobileEntryPoint() { - await openMobileAgentSidebar(this.page); - await expectMobileAgentSidebarVisible(this.page); - const button = this.page.getByTestId("sidebar-import-session"); + /** The new workspace screen's Import session button, reached from the sidebar. */ + async revealNewWorkspaceEntryPoint() { + const compact = (this.page.viewportSize()?.width ?? 0) < 768; + if (compact) { + await openMobileAgentSidebar(this.page); + await expectMobileAgentSidebarVisible(this.page); + } + await openGlobalNewWorkspaceComposer(this.page); + const button = this.page.getByTestId("new-workspace-import-session"); await expect(button).toHaveAccessibleName("Import session"); await expect(button).toBeInViewport(); } async openGlobally() { - await expect(this.page.getByTestId("sidebar-import-session")).toBeVisible(); - await this.page.getByTestId("sidebar-import-session").click(); + await this.revealNewWorkspaceEntryPoint(); + await this.openFromNewWorkspace(); + } + async openFromNewWorkspace() { + await this.page.getByTestId("new-workspace-import-session").click(); await this.expectSheetReady(); } async openFromWorkspaceHeader() { @@ -108,8 +117,12 @@ export class ImportSessionFlow { }); } async expectTranscript(userText: string, assistantText: string) { - await expect(this.page.getByTestId("user-message")).toContainText(userText); - await expect(this.page.getByTestId("assistant-message")).toContainText(assistantText); + await expect(this.page.getByTestId("user-message").filter({ visible: true })).toContainText( + userText, + ); + await expect( + this.page.getByTestId("assistant-message").filter({ visible: true }), + ).toContainText(assistantText); } async showAll() { await this.page.getByTestId("import-session-show-all").click(); @@ -120,7 +133,9 @@ export class ImportSessionFlow { timeout: 30_000, }); const workspace = this.page.getByTestId(`workspace-deck-entry-${getServerId()}:${workspaceId}`); - await expect(workspace.getByTestId("user-message")).toContainText(userText); + await expect(workspace.getByTestId("user-message").filter({ visible: true })).toContainText( + userText, + ); } async close() { await this.page.keyboard.press("Escape"); diff --git a/packages/app/e2e/support/helpers/new-workspace.ts b/packages/app/e2e/support/helpers/new-workspace.ts index dace580491..63034d52a3 100644 --- a/packages/app/e2e/support/helpers/new-workspace.ts +++ b/packages/app/e2e/support/helpers/new-workspace.ts @@ -198,7 +198,8 @@ export async function openNewWorkspaceComposer( } export async function openGlobalNewWorkspaceComposer(page: Page): Promise { - await page.getByTestId("sidebar-global-new-workspace").click(); + // The shell keeps a hidden copy of the other layout's sidebar mounted. + await page.locator('[data-testid="sidebar-global-new-workspace"]:visible').first().click(); await expect(page).toHaveURL(/\/new(?:\?.*)?$/, { timeout: 30_000, diff --git a/packages/app/e2e/support/helpers/sidebar-nav-settings.ts b/packages/app/e2e/support/helpers/sidebar-nav-settings.ts index b17b7b6bfb..6631d839cb 100644 --- a/packages/app/e2e/support/helpers/sidebar-nav-settings.ts +++ b/packages/app/e2e/support/helpers/sidebar-nav-settings.ts @@ -220,32 +220,32 @@ export async function expectFooterItemHidden(page: Page, key: string): Promise { - const boxes = await Promise.all( - FOOTER_ICON_TEST_IDS.map((testID) => - page.locator(`[data-testid="${testID}"]:visible`).first().boundingBox(), - ), - ); - const [first, ...rest] = boxes.map((box) => box!); - expect(first.width).toBeGreaterThan(rest[0]!.width); - await expect( - page - .locator('[data-testid="sidebar-add-project"]:visible') - .getByText("Add project", { exact: true }), - ).toBeVisible(); - let previous = first; - for (const box of rest) { - expect(Math.abs(box.y + box.height / 2 - first.y - first.height / 2)).toBeLessThan(2); - expect(box.x).toBeGreaterThan(previous.x); - previous = box; + const boxes = ( + await Promise.all( + FOOTER_ICON_TEST_IDS.map((testID) => + page.locator(`[data-testid="${testID}"]:visible`).first().boundingBox(), + ), + ) + ).map((box) => box!); + const [first] = boxes; + for (const box of boxes) { + expect(Math.abs(box.y + box.height / 2 - first!.y - first!.height / 2)).toBeLessThan(2); + expect(box.width).toBe(first!.width); } + const gaps = boxes.slice(1).map((box, index) => box.x - (boxes[index]!.x + boxes[index]!.width)); + expect(gaps).toEqual([0, 0, expect.any(Number), 0]); + expect(gaps[2]).toBeGreaterThan(first!.width); } export async function expectFooterSeparator(page: Page, shown: boolean): Promise { diff --git a/packages/app/e2e/support/helpers/startup-dsl.ts b/packages/app/e2e/support/helpers/startup-dsl.ts index dc65b63e08..19dbeecdd0 100644 --- a/packages/app/e2e/support/helpers/startup-dsl.ts +++ b/packages/app/e2e/support/helpers/startup-dsl.ts @@ -163,7 +163,6 @@ class StartupAssertions { await expect(hostRow).toBeVisible({ timeout: 15_000 }); await expect(hostRow).toContainText(input.label); await expect(this.page.getByTestId("sidebar-add-project")).toBeVisible(); - await expect(this.page.getByTestId("sidebar-import-session")).toBeVisible(); await expect(this.page.getByTestId("sidebar-settings")).toBeVisible(); await expect(this.page.getByTestId("welcome-screen")).toHaveCount(0); return this; diff --git a/packages/app/e2e/support/helpers/usage-sidebar-item.ts b/packages/app/e2e/support/helpers/usage-sidebar-item.ts index 342045cd1d..56d4455099 100644 --- a/packages/app/e2e/support/helpers/usage-sidebar-item.ts +++ b/packages/app/e2e/support/helpers/usage-sidebar-item.ts @@ -1,7 +1,59 @@ import { readFileSync } from "node:fs"; +import { mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; import path from "node:path"; import type { UsageReportEntry } from "@getpaseo/protocol/messages"; import { expect, type Locator, type Page } from "@playwright/test"; +import { connectNewWorkspaceDaemonClient } from "./new-workspace"; +import { pluginRequirements } from "./plugin-fixture"; + +/** Real usage-source plugin; its long report exercises the sheet's scrolling boundary. */ +export async function installTallUsageSource() { + const directory = await mkdtemp(path.join(tmpdir(), "paseo-tall-usage-")); + const client = await connectNewWorkspaceDaemonClient({ ownProjects: false }); + const previous = await client.getDaemonConfig(); + const cleanup = async () => { + try { + await client.removePlugin("tall-usage"); + await client.patchDaemonConfig({ pluginsEnabled: previous.config.pluginsEnabled ?? false }); + } finally { + await client.close(); + await rm(directory, { recursive: true, force: true }); + } + }; + try { + await writeFile( + path.join(directory, "paseo-plugin.json"), + JSON.stringify({ + id: "tall-usage", + requirements: pluginRequirements, + }), + ); + await writeFile( + path.join(directory, "index.server.ts"), + ` +import { z } from "zod"; +export default function contribute(server) { + server.registerUsageSource({ + id: "tall-usage", label: "Scrolling account", input: z.object({}), + discover: async () => [{}], + identify: async () => ({ key: "scrolling-account" }), + fetch: async () => ({ status: "available", windows: Array.from({ length: 20 }, (_, i) => ({ + id: String(i), label: "Window " + (i + 1), usedPct: 25, + })) }), + }); + return () => {}; +} +`, + ); + await client.patchDaemonConfig({ pluginsEnabled: true }); + await client.installPluginSource({ source: directory }); + return { cleanup }; + } catch (error) { + await cleanup(); + throw error; + } +} const PLUGINS_DIR = path.resolve(__dirname, "../../../../../plugins"); @@ -28,8 +80,22 @@ export function claudeAndCodexReports(): UsageReportEntry[] { status: "available", planLabel: "Max", windows: [ - { id: "five_hour", label: "Session", usedPct: 31, resetsAt: inOneDay() }, - { id: "weekly", label: "Weekly", usedPct: 54, resetsAt: inOneDay() }, + { + id: "five_hour", + label: "Session", + shortLabel: "5h", + summary: true, + usedPct: 31, + resetsAt: inOneDay(), + }, + { + id: "weekly", + label: "Weekly", + shortLabel: "wk", + summary: true, + usedPct: 54, + resetsAt: inOneDay(), + }, ], }, }, @@ -44,8 +110,22 @@ export function claudeAndCodexReports(): UsageReportEntry[] { status: "available", planLabel: "Pro", windows: [ - { id: "session", label: "Session", usedPct: 7, resetsAt: inOneDay() }, - { id: "weekly", label: "Weekly", usedPct: 12, resetsAt: inOneDay() }, + { + id: "session", + label: "Session", + shortLabel: "5h", + summary: true, + usedPct: 7, + resetsAt: inOneDay(), + }, + { + id: "weekly", + label: "Weekly", + shortLabel: "wk", + summary: true, + usedPct: 12, + resetsAt: inOneDay(), + }, ], }, }, @@ -71,9 +151,10 @@ export async function expectOnUsageScreen(page: Page): Promise { await expect(page).toHaveURL(/\/usage$/); } -export async function expectPinnedUsage(page: Page, percents: string[]): Promise { - const windows = usageItem(page).getByTestId("sidebar-usage-pinned-window"); - await expect(windows).toHaveText(percents); +/** Each pinned window as it reads: its percent and short label, "31% 5h". */ +export async function expectPinnedUsage(page: Page, windows: string[]): Promise { + const pinned = usageItem(page).getByTestId("sidebar-usage-pinned-window"); + await expect(pinned).toHaveText(windows); } /** Without pinned windows the Usage item is a plain row that reads "Usage". */ @@ -95,8 +176,21 @@ export async function togglePin(scope: Locator, source: string, window: string) await expect(row).toBeChecked({ checked: !pinned }); } -export async function showUsageAs(scope: Locator | Page, displayAs: "used" | "remaining") { - await scope.locator(`[data-testid="usage-display-${displayAs}"]:visible`).first().click(); +/** The usage title row's options menu: Refresh and Used/Remaining. */ +export async function openUsageOptions(page: Page): Promise { + await page.locator('[data-testid="usage-options-menu"]:visible').first().click(); + await expect(page.getByTestId("usage-display-used")).toBeVisible(); +} + +export async function showUsageAs(page: Page, displayAs: "used" | "remaining") { + await openUsageOptions(page); + await page.getByTestId(`usage-display-${displayAs}`).click(); + await expect(page.getByTestId("usage-display-used")).toHaveCount(0); +} + +export async function refreshAllUsage(page: Page): Promise { + await openUsageOptions(page); + await page.getByRole("menuitem", { name: "Refresh", exact: true }).click(); } /** Opens the compact sidebar drawer. */ diff --git a/packages/app/src/command-center/command-center.tsx b/packages/app/src/command-center/command-center.tsx index e909bf28e5..607f24c214 100644 --- a/packages/app/src/command-center/command-center.tsx +++ b/packages/app/src/command-center/command-center.tsx @@ -14,11 +14,7 @@ import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { Check, ChevronRight, Folder, X } from "lucide-react-native"; import { StyleSheet, withUnistyles } from "react-native-unistyles"; -import { - BottomSheetBackdrop, - BottomSheetFlatList, - type BottomSheetFlatListMethods, -} from "@gorhom/bottom-sheet"; +import { BottomSheetFlatList, type BottomSheetFlatListMethods } from "@gorhom/bottom-sheet"; import { AgentStatusDot } from "@/components/agent-status-dot"; import { MaterialFileIcon } from "@/components/material-file-icon"; import { LoadingSpinner } from "@/components/ui/loading-spinner"; @@ -718,12 +714,6 @@ export function CommandCenter() { layer: modalLayer, onKeyDown: handleWebOverlayKeyDown, }); - const backdrop = useCallback( - (props: React.ComponentProps) => ( - - ), - [], - ); if (showBottomSheet) { return ( @@ -735,7 +725,7 @@ export function CommandCenter() { enableDynamicSizing={false} onChange={handleSheetChange} onDismiss={handleSheetDismiss} - backdropComponent={backdrop} + backdropOpacity={0.45} enablePanDownToClose backgroundStyle={styles.sheetBackground} handleIndicatorStyle={styles.sheetHandle} diff --git a/packages/app/src/components/adaptive-modal-sheet.tsx b/packages/app/src/components/adaptive-modal-sheet.tsx index 78cdfd2e7a..4ceb43d917 100644 --- a/packages/app/src/components/adaptive-modal-sheet.tsx +++ b/packages/app/src/components/adaptive-modal-sheet.tsx @@ -13,7 +13,6 @@ import { useWebOverlayRegistration, } from "../lib/overlay-root"; import { - BottomSheetBackdrop, KEYBOARD_STATUS, useBottomSheetInternal, type BottomSheetBackgroundProps, @@ -550,13 +549,6 @@ export function AdaptiveModalSheet({ onDismiss?.(); }, [handleSheetDismiss, onDismiss]); - const renderBackdrop = useCallback( - (props: React.ComponentProps) => ( - - ), - [], - ); - const desktopCardStyle = useMemo( () => [ styles.desktopCard, @@ -649,7 +641,7 @@ export function AdaptiveModalSheet({ enableDynamicSizing={false} onChange={handleSheetChange} onDismiss={handleDismiss} - backdropComponent={renderBackdrop} + backdropOpacity={0.45} enablePanDownToClose backgroundComponent={SheetBackground} handleIndicatorStyle={handleIndicatorStyle} diff --git a/packages/app/src/components/left-sidebar.tsx b/packages/app/src/components/left-sidebar.tsx index c2bff3febc..8095497ec7 100644 --- a/packages/app/src/components/left-sidebar.tsx +++ b/packages/app/src/components/left-sidebar.tsx @@ -1,5 +1,5 @@ import { router } from "expo-router"; -import { FolderPlus, GitBranch, Import, Server, Settings, X } from "lucide-react-native"; +import { CircleGauge, FolderPlus, GitBranch, Server, Settings, X } from "lucide-react-native"; import { useTranslation } from "react-i18next"; import { memo, useCallback, useEffect, useMemo, useRef, useState, type RefObject } from "react"; import { @@ -8,7 +8,6 @@ import { Text, useWindowDimensions, View, - type PressableStateCallbackType, } from "react-native"; import { Gesture } from "react-native-gesture-handler"; import Animated, { runOnJS, useAnimatedStyle, useSharedValue } from "react-native-reanimated"; @@ -45,6 +44,7 @@ import type { SidebarProjectIconTarget } from "@/utils/sidebar-project-row-model import { type SidebarGroupMode, useSidebarViewStore } from "@/stores/sidebar-view-store"; import { useHosts } from "@/runtime/host-runtime"; import { PluginSidebarItem } from "@/plugins/sidebar-items"; +import { builtinSidebarNavLabelKey } from "@/sidebar-nav/model"; import { useSidebarNavItems } from "@/sidebar-nav/use-sidebar-nav-items"; import { usePanelStore } from "@/stores/panel-store"; import { useOwnsWindowChromeCorner, WindowChromeSafeArea } from "@/utils/desktop-window"; @@ -52,7 +52,7 @@ import { useCloseAgentListGesture } from "@/mobile-panels/gestures"; import { MobilePanelOverlay } from "@/mobile-panels/presentation"; import { buildSettingsAddHostRoute, buildSettingsRoute } from "@/utils/host-routes"; import { openHostOverview } from "@/navigation/settings-navigation"; -import { UsageSidebarItem } from "@/usage"; +import { UsageSidebarItem, useOpenUsageScreen } from "@/usage"; import { SidebarAgentListSkeleton } from "./sidebar-agent-list-skeleton"; import { SidebarCalloutSlot } from "./sidebar-callout-slot"; import { SidebarWorkspaceList } from "./sidebar-workspace-list"; @@ -89,9 +89,9 @@ interface SidebarSharedProps { interface SidebarLabels { addProject: string; hosts: string; - importSession: string; settings: string; searchHosts: string; + usage: string; closeSidebar: string; } @@ -196,9 +196,9 @@ export const LeftSidebar = memo(function LeftSidebar({ active }: { active: boole (): SidebarLabels => ({ addProject: t("sidebar.actions.addProject"), hosts: t("sidebar.actions.hosts"), - importSession: t("importSession.title"), settings: t("sidebar.actions.settings"), searchHosts: t("sidebar.host.searchPlaceholder"), + usage: t(builtinSidebarNavLabelKey("usage")), closeSidebar: t("sidebar.actions.closeSidebar"), }), [t], @@ -283,6 +283,7 @@ function FooterIconButton({ testID: string; label: string; icon: typeof FolderPlus; + /** Only for a glyph that reads larger than the others at the same size. */ iconSize?: number; shortcutKeys?: ReturnType; theme: SidebarTheme; @@ -317,64 +318,6 @@ function FooterIconButton({ ); } -function footerAddProjectButtonStyle({ - hovered, -}: PressableStateCallbackType & { hovered?: boolean }) { - return [styles.footerAddProjectButton, Boolean(hovered) && styles.footerAddProjectButtonHovered]; -} - -function FooterAddProjectButton({ - onPress, - label, - shortcutKeys, - theme, -}: { - onPress: () => void; - label: string; - shortcutKeys: ReturnType; - theme: SidebarTheme; -}) { - return ( - - - - {({ hovered }) => { - const isHovered = Boolean(hovered); - return ( - <> - - - {label} - - - ); - }} - - - - - - - ); -} - function SidebarHostPicker({ theme, label, @@ -423,7 +366,8 @@ function SidebarHostPicker({ testID="sidebar-hosts-trigger" label={label} icon={Server} - iconSize={theme.iconSize.sm} + // Server's two boxes fill more of the square than the other glyphs. + iconSize={theme.iconSize.md - 1} theme={theme} /> @@ -448,7 +392,6 @@ function IconTooltipContent({ function SidebarFooter({ theme, handleOpenProject, - handleImportSession, handleSettings, labels, handleAddHost, @@ -457,14 +400,13 @@ function SidebarFooter({ }: { theme: SidebarTheme; handleOpenProject: () => void; - handleImportSession: () => void; handleSettings: () => void; labels: { addProject: string; hosts: string; - importSession: string; settings: string; searchHosts: string; + usage: string; }; handleAddHost: () => void; handleOpenHostSettings: (serverId: string) => void; @@ -472,41 +414,44 @@ function SidebarFooter({ }) { const newAgentKeys = useShortcutKeys("new-agent"); const settingsKeys = useShortcutKeys("toggle-settings"); + const openUsageScreen = useOpenUsageScreen(); + // One line of icons: Add project, Usage, Hosts, then Help and Settings at the end. return ( - - - - - - - + + + + + ); @@ -593,7 +538,11 @@ function MobileSidebar({ - + ({ right: 0, zIndex: 2, alignItems: "flex-end", - pointerEvents: "box-none", }, mobileCloseButton: { // The 16px X paints farther inside its 32px hit target than the 14px Settings2 glyph. @@ -971,48 +917,22 @@ const styles = StyleSheet.create((theme) => ({ borderTopWidth: 1, borderTopColor: theme.colors.border, }, + // The icons' 28pt buttons sit edge to edge; their own inset spaces the glyphs. sidebarFooter: { flexDirection: "row", alignItems: "center", - gap: theme.spacing[2], paddingHorizontal: theme.spacing[2], - paddingVertical: theme.spacing[3], - }, - footerIconRow: { - flexDirection: "row", - alignItems: "center", - gap: theme.spacing[2], - flexShrink: 0, - }, - footerAddProjectButton: { - minWidth: 0, - minHeight: 32, - flex: 1, - flexDirection: "row", - alignItems: "center", - gap: theme.spacing[2], paddingVertical: theme.spacing[1.5], - paddingHorizontal: theme.spacing[2], - borderRadius: theme.borderRadius.lg, }, - footerAddProjectButtonHovered: { - backgroundColor: theme.colors.surfaceSidebarHover, + // Pushes Help and Settings to the end of the icon line. + footerSpacer: { + flex: 1, }, - footerAddProjectLabel: { - minWidth: 0, - flexShrink: 1, - fontSize: theme.fontSize.base, - fontWeight: theme.fontWeight.normal, - color: theme.colors.foregroundMuted, - }, - footerAddProjectLabelHovered: { - color: theme.colors.foreground, - }, - // Usage and plugin rows sit above the restored footer line. + // Usage and plugin rows sit above the footer's icon line, spaced like the header nav rows. footerRows: { paddingHorizontal: theme.spacing[2], - paddingVertical: theme.spacing[3], - gap: theme.spacing[1], + paddingVertical: theme.spacing[1.5], + gap: 2, }, footerIconButton: { width: 28, diff --git a/packages/app/src/components/sidebar/sidebar-header-row.tsx b/packages/app/src/components/sidebar/sidebar-header-row.tsx index f59672d388..4519f6d4ff 100644 --- a/packages/app/src/components/sidebar/sidebar-header-row.tsx +++ b/packages/app/src/components/sidebar/sidebar-header-row.tsx @@ -89,7 +89,7 @@ export function SidebarHeaderRow({ accessibilityLabel={accessibilityLabel ?? label} accessibilityState={isActive ? SELECTED_STATE : undefined} aria-selected={isActive} - style={variant === "inline" ? styles.buttonInline : styles.button} + style={styles.button} > {ThemedIcon ? ( ({ // edge with the list below. paddingHorizontal: theme.spacing[2], }, - // Footer rows put their icon on the footer's rail: where a 16px icon sits in a 28px button. - buttonInline: { - flex: 1, - minWidth: 0, - flexDirection: "row", - alignItems: "center", - gap: theme.spacing[2], - minHeight: 28, - paddingVertical: theme.spacing[1], - paddingHorizontal: theme.spacing[1.5], - }, iconSpacer: { width: ICON_SIZE.md, height: ICON_SIZE.md }, iconSpacerCompact: { width: ICON_SIZE.sm, height: ICON_SIZE.sm }, label: { diff --git a/packages/app/src/components/tool-call-sheet.tsx b/packages/app/src/components/tool-call-sheet.tsx index 022a9bc7c2..33009ee376 100644 --- a/packages/app/src/components/tool-call-sheet.tsx +++ b/packages/app/src/components/tool-call-sheet.tsx @@ -3,7 +3,7 @@ import type { ReactNode } from "react"; import { View, Text, Pressable } from "react-native"; import { StyleSheet, withUnistyles } from "react-native-unistyles"; import { useTranslation } from "react-i18next"; -import { BottomSheetScrollView, BottomSheetBackdrop } from "@gorhom/bottom-sheet"; +import { BottomSheetScrollView } from "@gorhom/bottom-sheet"; import { X } from "lucide-react-native"; import type { ToolCallDetail } from "@getpaseo/protocol/agent-types"; import { @@ -115,13 +115,6 @@ export function ToolCallSheetProvider({ children }: ToolCallSheetProviderProps) setSheetData(null); }, [handleSheetDismiss]); - const renderBackdrop = useCallback( - (props: React.ComponentProps) => ( - - ), - [], - ); - const contextValue = useMemo( () => ({ openToolCall, closeToolCall }), [openToolCall, closeToolCall], @@ -138,7 +131,7 @@ export function ToolCallSheetProvider({ children }: ToolCallSheetProviderProps) enableDynamicSizing={false} onChange={handleSheetChange} onDismiss={handleToolCallSheetDismiss} - backdropComponent={renderBackdrop} + backdropOpacity={0.5} enablePanDownToClose > {sheetData && } diff --git a/packages/app/src/components/ui/combobox.tsx b/packages/app/src/components/ui/combobox.tsx index 74748e423a..736ca3bfa5 100644 --- a/packages/app/src/components/ui/combobox.tsx +++ b/packages/app/src/components/ui/combobox.tsx @@ -28,11 +28,7 @@ import { useSafeAreaInsets } from "react-native-safe-area-context"; import type { EditingTextInputHandle } from "@/components/ui/text-input"; import { StyleSheet, useUnistyles } from "react-native-unistyles"; import { useIsCompactFormFactor } from "@/constants/layout"; -import { - BottomSheetScrollView, - BottomSheetBackdrop, - BottomSheetBackgroundProps, -} from "@gorhom/bottom-sheet"; +import { BottomSheetScrollView, BottomSheetBackgroundProps } from "@gorhom/bottom-sheet"; import Animated, { FadeIn, FadeOut } from "react-native-reanimated"; import { GestureHandlerRootView } from "react-native-gesture-handler"; import { Check, File, Folder, Search } from "lucide-react-native"; @@ -948,18 +944,6 @@ interface MobileBodyProps { } function MobileComboboxBody(props: MobileBodyProps): ReactElement { - const renderBackdrop = useCallback( - (backdropProps: React.ComponentProps) => ( - - ), - [], - ); - const comboboxTitleStyle = useMemo( () => [styles.comboboxTitle, { color: props.titleColor }], [props.titleColor], @@ -991,7 +975,7 @@ function MobileComboboxBody(props: MobileBodyProps): ReactElement { enableDynamicSizing={false} onChange={props.handleSheetChange} onDismiss={props.handleSheetDismiss} - backdropComponent={renderBackdrop} + backdropOpacity={0.45} enablePanDownToClose backgroundComponent={ComboboxSheetBackground} handleIndicatorStyle={props.handleIndicatorStyle} diff --git a/packages/app/src/components/ui/isolated-bottom-sheet-modal/index.tsx b/packages/app/src/components/ui/isolated-bottom-sheet-modal/index.tsx index f7869823b8..3f9c9113d0 100644 --- a/packages/app/src/components/ui/isolated-bottom-sheet-modal/index.tsx +++ b/packages/app/src/components/ui/isolated-bottom-sheet-modal/index.tsx @@ -1,9 +1,10 @@ import { BottomSheetModal as GorhomBottomSheetModal, type BottomSheetModalProps, + type BottomSheetBackdropProps, } from "@gorhom/bottom-sheet"; import React from "react"; -import { forwardRef, useCallback, useEffect, useMemo, useRef } from "react"; +import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef } from "react"; import type { ElementRef, ReactNode } from "react"; import { systemBackPress } from "./back-press"; import { @@ -11,6 +12,7 @@ import { createBottomSheetVisibilityTracker, } from "./visibility-tracker"; import { BottomSheetScope } from "@/components/ui/bottom-sheet-scope"; +import { SheetBackdrop } from "./sheet-backdrop"; type GorhomBottomSheetModalMethods = ElementRef; @@ -36,7 +38,7 @@ export type ContextBridge = (children: ReactNode) => ReactNode; type IsolatedBottomSheetModalProps = Omit< BottomSheetModalProps, - "enableDismissOnClose" | "stackBehavior" | "children" + "enableDismissOnClose" | "stackBehavior" | "children" | "backdropComponent" > & { /** * Nodes only. Gorhom also accepts a render function, but nothing here uses it and a bridge @@ -44,6 +46,8 @@ type IsolatedBottomSheetModalProps = Omit< */ children?: ReactNode; presentation?: "push" | "replace"; + /** Show a dismissible backdrop. Its interaction belongs to the modal stack. */ + backdropOpacity?: number; /** * Required, and `null` is a real answer: a sheet that needs nothing from its call site should * have to say so. The failure it prevents is invisible until someone adds a `useContext` deep @@ -58,13 +62,43 @@ export const IsolatedBottomSheetModal = forwardRef< IsolatedBottomSheetModalRef, IsolatedBottomSheetModalProps >(function IsolatedBottomSheetModal(props, ref) { - const { children, presentation = "push", contextBridge, ...bottomSheetProps } = props; + // Gorhom puts this ref into its provider queue and reads `.current` for stack operations. + // A callback ref accepts the handle but leaves that queue unable to dismiss or restore it. + const modalRef = useRef(null); + useImperativeHandle( + ref, + () => ({ + present: (...args) => modalRef.current?.present(...args), + dismiss: (...args) => modalRef.current?.dismiss(...args), + snapToIndex: (...args) => modalRef.current?.snapToIndex(...args), + snapToPosition: (...args) => modalRef.current?.snapToPosition(...args), + expand: (...args) => modalRef.current?.expand(...args), + collapse: (...args) => modalRef.current?.collapse(...args), + close: (...args) => modalRef.current?.close(...args), + forceClose: (...args) => modalRef.current?.forceClose(...args), + }), + [], + ); + const { + children, + presentation = "push", + contextBridge, + backdropOpacity, + ...bottomSheetProps + } = props; + const renderBackdrop = useCallback( + (backdropProps: BottomSheetBackdropProps) => ( + + ), + [backdropOpacity], + ); const modal = ( {contextBridge ? contextBridge(children) : children} diff --git a/packages/app/src/components/ui/isolated-bottom-sheet-modal/sheet-backdrop.tsx b/packages/app/src/components/ui/isolated-bottom-sheet-modal/sheet-backdrop.tsx new file mode 100644 index 0000000000..1232566462 --- /dev/null +++ b/packages/app/src/components/ui/isolated-bottom-sheet-modal/sheet-backdrop.tsx @@ -0,0 +1,5 @@ +import { BottomSheetBackdrop, type BottomSheetBackdropProps } from "@gorhom/bottom-sheet"; + +export function SheetBackdrop(props: BottomSheetBackdropProps & { opacity?: number }) { + return ; +} diff --git a/packages/app/src/components/ui/isolated-bottom-sheet-modal/sheet-backdrop.web.tsx b/packages/app/src/components/ui/isolated-bottom-sheet-modal/sheet-backdrop.web.tsx new file mode 100644 index 0000000000..ba1e397a9e --- /dev/null +++ b/packages/app/src/components/ui/isolated-bottom-sheet-modal/sheet-backdrop.web.tsx @@ -0,0 +1,36 @@ +import { + BottomSheetBackdrop, + useBottomSheetModal, + type BottomSheetBackdropProps, +} from "@gorhom/bottom-sheet"; +import { useCallback } from "react"; +import { useTranslation } from "react-i18next"; +import { Pressable, StyleSheet, View } from "react-native"; + +export function SheetBackdrop(props: BottomSheetBackdropProps & { opacity?: number }) { + const { dismiss } = useBottomSheetModal(); + const { t } = useTranslation(); + const dismissTopSheet = useCallback(() => dismiss(), [dismiss]); + + return ( + + + + + ); +} diff --git a/packages/app/src/components/ui/menu/menu-surface.tsx b/packages/app/src/components/ui/menu/menu-surface.tsx index 3a2729ad8e..0b5a6a77ac 100644 --- a/packages/app/src/components/ui/menu/menu-surface.tsx +++ b/packages/app/src/components/ui/menu/menu-surface.tsx @@ -5,14 +5,13 @@ import { useEffect, useMemo, useRef, - type ComponentProps, type ReactElement, type ReactNode, } from "react"; import { useTranslation } from "react-i18next"; -import { Pressable, Text, View } from "react-native"; +import { Pressable, Text, View, useWindowDimensions } from "react-native"; import { StyleSheet, withUnistyles } from "react-native-unistyles"; -import { BottomSheetBackdrop, BottomSheetScrollView } from "@gorhom/bottom-sheet"; +import { BottomSheetScrollView } from "@gorhom/bottom-sheet"; import { ChevronLeft } from "lucide-react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { @@ -345,6 +344,7 @@ function MenuSheetSurface({ testID, keyboardFocusScope, }: MenuSurfaceProps): ReactElement | null { + const { height: windowHeight } = useWindowDimensions(); const menu = useMenuContext("MenuSurface"); const { value: surfaceValue } = useSubAnchors(); const safeAreaInsets = useSafeAreaInsets(); @@ -370,18 +370,6 @@ function MenuSheetSurface({ onClose: handleClose, }); - const renderBackdrop = useCallback( - (backdropProps: ComponentProps) => ( - - ), - [], - ); - const openPageId = currentPageId(menu.path); const openPage = openPageId ? pages.find((page) => page.id === openPageId) : null; const depth = menu.path.length; @@ -405,9 +393,10 @@ function MenuSheetSurface({ // Content-sized rather than fixed snap points: a pushed page is rarely the same height // as the page it replaced, and a fixed sheet would either clip it or leave dead space. enableDynamicSizing + maxDynamicContentSize={windowHeight * 0.8} onChange={handleSheetChange} onDismiss={handleSheetDismiss} - backdropComponent={renderBackdrop} + backdropOpacity={0.45} enablePanDownToClose // `interactive` rather than `extend`, which is what every other sheet in the app uses. // `extend` grows the sheet to its largest snap point, and with `enableDynamicSizing` that diff --git a/packages/app/src/i18n/resources/ar.ts b/packages/app/src/i18n/resources/ar.ts index 07f3a6d00d..d29de2b33d 100644 --- a/packages/app/src/i18n/resources/ar.ts +++ b/packages/app/src/i18n/resources/ar.ts @@ -26,6 +26,7 @@ export const ar: TranslationResources = { total: "{{total}} تطابقات", }, common: { + bottomSheetBackdrop: "خلفية اللوحة السفلية", back: "خلف", loading: "تحميل...", actions: { diff --git a/packages/app/src/i18n/resources/en.ts b/packages/app/src/i18n/resources/en.ts index 9230ef3155..1e2d4c3f10 100644 --- a/packages/app/src/i18n/resources/en.ts +++ b/packages/app/src/i18n/resources/en.ts @@ -22,6 +22,7 @@ export const en = { total: "{{total}} matches", }, common: { + bottomSheetBackdrop: "Bottom sheet backdrop", back: "Back", loading: "Loading...", actions: { diff --git a/packages/app/src/i18n/resources/es.ts b/packages/app/src/i18n/resources/es.ts index c61778633d..cc37432b5b 100644 --- a/packages/app/src/i18n/resources/es.ts +++ b/packages/app/src/i18n/resources/es.ts @@ -27,6 +27,7 @@ export const es: TranslationResources = { total: "{{total}} coincidencias", }, common: { + bottomSheetBackdrop: "Fondo del panel inferior", back: "Atrás", loading: "Cargando...", actions: { diff --git a/packages/app/src/i18n/resources/fr.ts b/packages/app/src/i18n/resources/fr.ts index 6e6999e5f8..aaf2608f9e 100644 --- a/packages/app/src/i18n/resources/fr.ts +++ b/packages/app/src/i18n/resources/fr.ts @@ -27,6 +27,7 @@ export const fr: TranslationResources = { total: "{{total}} résultats", }, common: { + bottomSheetBackdrop: "Arrière-plan du panneau inférieur", back: "Dos", loading: "Chargement...", actions: { diff --git a/packages/app/src/i18n/resources/ja.ts b/packages/app/src/i18n/resources/ja.ts index 8e59c63234..3491b507a2 100644 --- a/packages/app/src/i18n/resources/ja.ts +++ b/packages/app/src/i18n/resources/ja.ts @@ -27,6 +27,7 @@ export const ja: TranslationResources = { total: "{{total}} 件の一致", }, common: { + bottomSheetBackdrop: "ボトムシートの背景", back: "戻る", loading: "読み込み中...", actions: { diff --git a/packages/app/src/i18n/resources/ko.ts b/packages/app/src/i18n/resources/ko.ts index 0f9437720d..7afe54b158 100644 --- a/packages/app/src/i18n/resources/ko.ts +++ b/packages/app/src/i18n/resources/ko.ts @@ -26,6 +26,7 @@ export const ko: TranslationResources = { total: "일치 항목 {{total}}개", }, common: { + bottomSheetBackdrop: "하단 시트 배경", back: "뒤로", loading: "불러오는 중...", actions: { diff --git a/packages/app/src/i18n/resources/pt-BR.ts b/packages/app/src/i18n/resources/pt-BR.ts index 75249797a2..a08088b2e4 100644 --- a/packages/app/src/i18n/resources/pt-BR.ts +++ b/packages/app/src/i18n/resources/pt-BR.ts @@ -27,6 +27,7 @@ export const ptBR: TranslationResources = { total: "{{total}} correspondências", }, common: { + bottomSheetBackdrop: "Fundo do painel inferior", back: "Voltar", loading: "Carregando...", actions: { diff --git a/packages/app/src/i18n/resources/ru.ts b/packages/app/src/i18n/resources/ru.ts index af11db8bf1..4526784896 100644 --- a/packages/app/src/i18n/resources/ru.ts +++ b/packages/app/src/i18n/resources/ru.ts @@ -27,6 +27,7 @@ export const ru: TranslationResources = { total: "Совпадений: {{total}}", }, common: { + bottomSheetBackdrop: "Фон нижней панели", back: "Назад", loading: "Загрузка...", actions: { diff --git a/packages/app/src/i18n/resources/zh-CN.ts b/packages/app/src/i18n/resources/zh-CN.ts index 3bc8727cb4..d13007dbea 100644 --- a/packages/app/src/i18n/resources/zh-CN.ts +++ b/packages/app/src/i18n/resources/zh-CN.ts @@ -26,6 +26,7 @@ export const zhCN: TranslationResources = { total: "{{total}} 个匹配项", }, common: { + bottomSheetBackdrop: "底部面板背景", back: "返回", loading: "加载中...", actions: { diff --git a/packages/app/src/screens/new-workspace-screen.tsx b/packages/app/src/screens/new-workspace-screen.tsx index 9e0d25144c..c919ab476d 100644 --- a/packages/app/src/screens/new-workspace-screen.tsx +++ b/packages/app/src/screens/new-workspace-screen.tsx @@ -122,6 +122,8 @@ import { } from "./new-workspace-initial-context"; import { buildNewWorkspaceProjectIconTargets } from "./new-workspace/project-icon-targets"; import { useNewWorkspaceProjectPicker } from "./new-workspace/project-picker"; +import { ImportSessionButton } from "./new-workspace/import-session-button"; +import { useImportSession } from "@/hooks/use-import-session"; import { buildTerminalsQueryKey, type ListTerminalsPayload, @@ -2367,6 +2369,7 @@ export function NewWorkspaceScreen({ }); const screenHeaderLeft = useMemo(() => , []); + const importSession = useImportSession({ serverId: selectedServerId }); const composer = isTerminalLaunch ? ( {composer} {errorMessage ? {errorMessage} : null} + {importSession.sheet} ); } @@ -2448,15 +2453,20 @@ function NewWorkspaceLayout({ isCompact, title, formStack, + onImportSession, children, }: { isCompact: boolean; title: string; formStack: ReactNode; + onImportSession: () => void; children: ReactNode; }) { + // At the top of the screen on compact layouts, under the composer otherwise. + const importSessionButton = ; const setupFields = ( <> + {isCompact ? {importSessionButton} : null} {title} @@ -2466,7 +2476,10 @@ function NewWorkspaceLayout({ return ( {setupFields} - {children} + <> + {children} + {isCompact ? null : importSessionButton} + ); } @@ -2481,6 +2494,12 @@ const styles = StyleSheet.create((theme) => ({ position: "relative", flex: 1, }, + // Takes the free space above the setup fields, so its button sits at the top of the screen. + // The inset puts the ghost button's icon on the setup rows' icon rail. + compactTopActions: { + flex: 1, + paddingHorizontal: theme.spacing[3], + }, composerTitleContainer: { marginBottom: theme.spacing[8], paddingLeft: theme.spacing[6], @@ -2503,7 +2522,8 @@ const styles = StyleSheet.create((theme) => ({ formStackDesktop: { flexDirection: "row", alignItems: "center", - marginBottom: theme.spacing[8], + // Matches the gap between the composer and the Import session pill below it. + marginBottom: theme.spacing[4], // The badge adds its own left padding; offset it so the project icon's left // edge lands exactly on the "New workspace" title's left edge. The trailing // inset mirrors it so the launch chip stops on the composer's inner content diff --git a/packages/app/src/screens/new-workspace/import-session-button.tsx b/packages/app/src/screens/new-workspace/import-session-button.tsx new file mode 100644 index 0000000000..39b55e1b2e --- /dev/null +++ b/packages/app/src/screens/new-workspace/import-session-button.tsx @@ -0,0 +1,50 @@ +import { Import } from "lucide-react-native"; +import { useTranslation } from "react-i18next"; +import { View } from "react-native"; +import { StyleSheet } from "react-native-unistyles"; +import { Button } from "@/components/ui/button"; + +/** + * The new workspace screen's way into Import session: a ghost button at the top on compact + * layouts, an outlined pill under the composer otherwise. Both are muted until hovered. + */ +export function ImportSessionButton({ + compact, + onPress, +}: { + compact: boolean; + onPress: () => void; +}) { + const { t } = useTranslation(); + return ( + + + + ); +} + +const styles = StyleSheet.create((theme) => ({ + compact: { + alignItems: "flex-start", + }, + // The composer sits spacing[4] in from the dock and leaves spacing[4] below itself; the pill's + // border lines up with the composer's border. + wide: { + alignItems: "flex-start", + paddingHorizontal: theme.spacing[4], + }, + pill: { + borderRadius: theme.borderRadius.full, + borderColor: theme.colors.borderAccent, + }, +})); diff --git a/packages/app/src/sidebar-nav/model.test.ts b/packages/app/src/sidebar-nav/model.test.ts index 4211a44410..f58acba007 100644 --- a/packages/app/src/sidebar-nav/model.test.ts +++ b/packages/app/src/sidebar-nav/model.test.ts @@ -34,6 +34,7 @@ describe("resolveSidebarNavItems", () => { it("yields builtins then plugins, all visible, when nothing is stored", () => { const items = resolveSidebarNavItems({ section: "header", + compact: false, pluginGroups: [kanban, notes], preferences: [], }); @@ -58,6 +59,7 @@ describe("resolveSidebarNavItems", () => { it("keeps the stored order and appends newly available items as visible", () => { const items = resolveSidebarNavItems({ section: "header", + compact: false, pluginGroups: [notes, kanban], preferences: [ { key: kanbanKey, visible: false }, @@ -79,6 +81,7 @@ describe("resolveSidebarNavItems", () => { it("skips keys that are unknown or not currently available", () => { const items = resolveSidebarNavItems({ section: "header", + compact: false, pluginGroups: [], preferences: [ { key: notesKey, visible: false }, @@ -98,6 +101,7 @@ describe("resolveSidebarNavItems", () => { it("lets the first of duplicate keys win", () => { const items = resolveSidebarNavItems({ section: "header", + compact: false, pluginGroups: [], preferences: [ { key: "history", visible: false }, @@ -114,10 +118,26 @@ describe("resolveSidebarNavItems", () => { }); }); +describe("resolveSidebarNavItems on compact layouts", () => { + it("hides the Usage item until it is turned on", () => { + const resolve = (compact: boolean, preferences: SidebarNavPreference[]) => + summarize( + resolveSidebarNavItems({ section: "footer", compact, pluginGroups: [], preferences }), + ); + + expect(resolve(true, [])).toEqual([{ key: "usage", visible: false }]); + expect(resolve(false, [])).toEqual([{ key: "usage", visible: true }]); + expect(resolve(true, [{ key: "usage", visible: true }])).toEqual([ + { key: "usage", visible: true }, + ]); + }); +}); + describe("setSidebarNavItemVisible", () => { it("toggles one item and writes the full resolved order", () => { const items = resolveSidebarNavItems({ section: "header", + compact: false, pluginGroups: [kanban], preferences: [], }); @@ -140,6 +160,7 @@ describe("setSidebarNavItemVisible", () => { ]; const items = resolveSidebarNavItems({ section: "header", + compact: false, pluginGroups: [], preferences: previous, }); @@ -165,6 +186,7 @@ describe("setSidebarNavItemVisible", () => { ]; const items = resolveSidebarNavItems({ section: "header", + compact: false, pluginGroups: [], preferences: previous, }); @@ -180,13 +202,23 @@ describe("setSidebarNavItemVisible", () => { ]); expect( summarize( - resolveSidebarNavItems({ section: "header", pluginGroups: [notes], preferences: next }), + resolveSidebarNavItems({ + section: "header", + compact: false, + pluginGroups: [notes], + preferences: next, + }), ), ).toEqual(next); }); it("returns the normalized list unchanged for an unknown key", () => { - const items = resolveSidebarNavItems({ section: "header", pluginGroups: [], preferences: [] }); + const items = resolveSidebarNavItems({ + section: "header", + compact: false, + pluginGroups: [], + preferences: [], + }); const next = setSidebarNavItemVisible({ items, key: "bogus", visible: false, previous: [] }); @@ -197,6 +229,7 @@ describe("setSidebarNavItemVisible", () => { describe("moveSidebarNavItem", () => { const items = resolveSidebarNavItems({ section: "header", + compact: false, pluginGroups: [kanban], preferences: [], }); @@ -274,6 +307,7 @@ describe("footer section", () => { it("resolves the Usage item first, then plugin rows", () => { const items = resolveSidebarNavItems({ section: "footer", + compact: false, pluginGroups: [sync], preferences: [], }); @@ -287,6 +321,7 @@ describe("footer section", () => { it("ignores header built-ins stored in footer preferences", () => { const items = resolveSidebarNavItems({ section: "footer", + compact: false, pluginGroups: [], preferences: [{ key: "history", visible: false }], }); @@ -297,6 +332,7 @@ describe("footer section", () => { it("ignores the footer icon buttons, which are fixed and not items", () => { const items = resolveSidebarNavItems({ section: "footer", + compact: false, pluginGroups: [sync], preferences: [ { key: "add-project", visible: false }, @@ -319,6 +355,7 @@ describe("footer section", () => { const previous: SidebarNavPreference[] = [notesPreference]; const items = resolveSidebarNavItems({ section: "footer", + compact: false, pluginGroups: [sync], preferences: previous, }); @@ -327,6 +364,7 @@ describe("footer section", () => { const hidden = setSidebarNavItemVisible({ items: resolveSidebarNavItems({ section: "footer", + compact: false, pluginGroups: [sync], preferences: moved, }), diff --git a/packages/app/src/sidebar-nav/model.ts b/packages/app/src/sidebar-nav/model.ts index 40e4db636f..b5da97c034 100644 --- a/packages/app/src/sidebar-nav/model.ts +++ b/packages/app/src/sidebar-nav/model.ts @@ -5,7 +5,7 @@ export type SidebarSection = PluginSidebarSection; /** * Each section's built-in items in their default order. The footer's bottom line - * (Add project and the Hosts, Import session, Help and support, Settings icons) is fixed. + * (Add project and the Hosts, Help and support, Settings icons) is fixed. */ export const BUILTIN_SIDEBAR_ITEM_IDS = { header: ["new-workspace", "history", "search", "schedules"], @@ -69,6 +69,16 @@ export function builtinSidebarNavShortcutAction(id: BuiltinSidebarItemId): strin return BUILTIN_SHORTCUT_ACTIONS[id]; } +/** + * Builtins that start hidden on compact layouts, until the user turns them on. A phone's footer + * has no room to spare for the Usage summary. + */ +const HIDDEN_BY_DEFAULT_ON_COMPACT: ReadonlySet = new Set(["usage"]); + +function builtinVisibleByDefault(id: BuiltinSidebarItemId, compact: boolean): boolean { + return !(compact && HIDDEN_BY_DEFAULT_ON_COMPACT.has(id)); +} + export function pluginSidebarNavKey( group: Pick, ): string { @@ -85,6 +95,8 @@ function isBuiltinSidebarItemId

( export function resolveSidebarNavItems
(input: { section: Section; + /** Compact layouts start some builtins hidden; a stored preference always wins. */ + compact: boolean; pluginGroups: readonly PluginSidebarGroup[]; preferences: readonly SidebarNavPreference[]; }): SidebarNavItem
[] { @@ -115,7 +127,12 @@ export function resolveSidebarNavItems
(input: { for (const id of builtinIds) { if (placed.has(id)) continue; - items.push({ kind: "builtin", key: id, id, visible: true }); + items.push({ + kind: "builtin", + key: id, + id, + visible: builtinVisibleByDefault(id, input.compact), + }); } for (const [key, group] of groupsByKey) { if (placed.has(key)) continue; diff --git a/packages/app/src/sidebar-nav/use-sidebar-nav-items.ts b/packages/app/src/sidebar-nav/use-sidebar-nav-items.ts index dcebf670df..96b99a6f1b 100644 --- a/packages/app/src/sidebar-nav/use-sidebar-nav-items.ts +++ b/packages/app/src/sidebar-nav/use-sidebar-nav-items.ts @@ -1,4 +1,5 @@ import { useCallback, useMemo } from "react"; +import { useIsCompactFormFactor } from "@/constants/layout"; import { useAppSettings } from "@/hooks/use-settings"; import type { AppSettings } from "@/hooks/use-settings/storage"; import { useInstalledPlugins } from "@/plugins/registry"; @@ -27,14 +28,15 @@ export function useSidebarNavItems
( section: Section, ): UseSidebarNavItemsReturn
{ const plugins = useInstalledPlugins(); + const compact = useIsCompactFormFactor(); const { settings, updateSettings } = useAppSettings(); const field = PREFERENCE_FIELDS[section]; const preferences = settings[field]; const pluginGroups = useMemo(() => groupPluginSidebarItems(plugins, section), [plugins, section]); const items = useMemo( - () => resolveSidebarNavItems({ section, pluginGroups, preferences }), - [pluginGroups, preferences, section], + () => resolveSidebarNavItems({ section, compact, pluginGroups, preferences }), + [compact, pluginGroups, preferences, section], ); const setVisible = useCallback( @@ -43,6 +45,7 @@ export function useSidebarNavItems
( const previous = current[field]; const currentItems = resolveSidebarNavItems({ section, + compact, pluginGroups, preferences: previous, }); @@ -51,7 +54,7 @@ export function useSidebarNavItems
( }; }); }, - [field, pluginGroups, section, updateSettings], + [compact, field, pluginGroups, section, updateSettings], ); const move = useCallback( @@ -60,6 +63,7 @@ export function useSidebarNavItems
( const previous = current[field]; const currentItems = resolveSidebarNavItems({ section, + compact, pluginGroups, preferences: previous, }); @@ -68,7 +72,7 @@ export function useSidebarNavItems
( }; }); }, - [field, pluginGroups, section, updateSettings], + [compact, field, pluginGroups, section, updateSettings], ); return { items, setVisible, move }; diff --git a/packages/app/src/tool-calls/detail-level/overview/sheet.tsx b/packages/app/src/tool-calls/detail-level/overview/sheet.tsx index 77990b3341..b3b5bf39eb 100644 --- a/packages/app/src/tool-calls/detail-level/overview/sheet.tsx +++ b/packages/app/src/tool-calls/detail-level/overview/sheet.tsx @@ -1,10 +1,6 @@ import { useCallback, useEffect, useRef, type ReactNode } from "react"; import { Pressable, Text, View } from "react-native"; -import { - BottomSheetBackdrop, - BottomSheetScrollView, - type BottomSheetScrollViewMethods, -} from "@gorhom/bottom-sheet"; +import { BottomSheetScrollView, type BottomSheetScrollViewMethods } from "@gorhom/bottom-sheet"; import { Wrench, X } from "lucide-react-native"; import { StyleSheet, withUnistyles } from "react-native-unistyles"; import { useTranslation } from "react-i18next"; @@ -38,12 +34,7 @@ export function OverviewToolCallGroupSheet({ visible, onClose, }); - const renderBackdrop = useCallback( - (props: React.ComponentProps) => ( - - ), - [], - ); + const scrollToLatest = useCallback(() => { scrollRef.current?.scrollToEnd({ animated: false }); }, []); @@ -64,7 +55,7 @@ export function OverviewToolCallGroupSheet({ enableDynamicSizing={false} onChange={handleSheetChange} onDismiss={handleSheetDismiss} - backdropComponent={renderBackdrop} + backdropOpacity={0.5} enablePanDownToClose > diff --git a/packages/app/src/usage/balance-bar.tsx b/packages/app/src/usage/balance-bar.tsx index 8bf3ad9d83..751193eec0 100644 --- a/packages/app/src/usage/balance-bar.tsx +++ b/packages/app/src/usage/balance-bar.tsx @@ -1,4 +1,5 @@ import { useMemo } from "react"; +import { useTranslation } from "react-i18next"; import { Text, View, type StyleProp, type ViewStyle } from "react-native"; import { StyleSheet } from "react-native-unistyles"; import { clampPct, formatAmount, formatResetLabel } from "./format"; @@ -9,19 +10,20 @@ interface ResolvedBalance { usedPct: number | null; } -function resolveBalance(balance: UsageBalance): ResolvedBalance { +function resolveBalance(balance: UsageBalance, locale: string): ResolvedBalance { const { used, remaining, limit, unit } = balance; + const format = (value: number) => formatAmount(value, unit, locale); if (limit != null && limit > 0) { const usedAmount = used ?? (remaining != null ? limit - remaining : null); const usedPct = usedAmount != null ? (usedAmount / limit) * 100 : null; - const usedText = usedAmount != null ? formatAmount(usedAmount, unit) : "—"; - return { amountText: `${usedText} / ${formatAmount(limit, unit)}`, usedPct }; + const usedText = usedAmount != null ? format(usedAmount) : "—"; + return { amountText: `${usedText} / ${format(limit)}`, usedPct }; } if (remaining != null) { - return { amountText: `${formatAmount(remaining, unit)} left`, usedPct: null }; + return { amountText: `${format(remaining)} left`, usedPct: null }; } if (used != null) { - return { amountText: formatAmount(used, unit), usedPct: null }; + return { amountText: format(used), usedPct: null }; } return { amountText: "—", usedPct: null }; } @@ -40,7 +42,8 @@ function fillToneStyle(tone: UsageTone) { } export function UsageBalanceBar({ balance }: { balance: UsageBalance }) { - const { amountText, usedPct } = resolveBalance(balance); + const { i18n } = useTranslation(); + const { amountText, usedPct } = resolveBalance(balance, i18n.language); const tone = balance.tone ?? "default"; const resetLabel = formatResetLabel(balance.resetsAt); diff --git a/packages/app/src/usage/controls.tsx b/packages/app/src/usage/controls.tsx index b7942fddd9..3fc29dd3e8 100644 --- a/packages/app/src/usage/controls.tsx +++ b/packages/app/src/usage/controls.tsx @@ -1,22 +1,13 @@ -import { RotateCw } from "lucide-react-native"; import { View } from "react-native"; -import { StyleSheet, withUnistyles } from "react-native-unistyles"; +import { StyleSheet } from "react-native-unistyles"; import { HostFilter } from "@/components/hosts/host-filter"; -import { extraMutedIconColorMapping } from "@/components/ui/icon-button-chrome"; -import { LoadingSpinner } from "@/components/ui/loading-spinner"; -import { ToolbarButton, paneContentToolbarIconSize } from "@/components/ui/pane-content-toolbar"; -import { useIsCompactFormFactor } from "@/constants/layout"; import { useMemo, type ReactElement } from "react"; -import { usageCopy } from "./copy"; import type { UsageDisplay } from "./display"; -import { UsageDisplayToggle } from "./display-toggle"; +import { UsageOptionsMenu } from "./options-menu"; import type { UsageHost } from "./model"; import { useHostUsage } from "./queries"; import type { UsageView } from "./types"; -const ThemedRotateCw = withUnistyles(RotateCw); -const ThemedLoadingSpinner = withUnistyles(LoadingSpinner); - /** The hosts to choose between, and which one is shown. */ export interface UsageHostSelection { hosts: UsageHost[]; @@ -26,8 +17,7 @@ export interface UsageHostSelection { /** * The controls on the right of every usage title row: the host filter when there is more than one - * host, the used/remaining toggle and Refresh. A host that cannot report usage keeps only the - * host filter. + * host, and the options menu. A host that cannot report usage keeps only the host filter. */ export function UsageControls({ view, @@ -41,8 +31,6 @@ export function UsageControls({ hostSelection?: UsageHostSelection; }) { const busy = view.kind === "loading" || (view.kind === "ready" && view.isRefreshing); - const compact = useIsCompactFormFactor(); - const iconSize = paneContentToolbarIconSize(compact); return ( {hostSelection && hostSelection.hosts.length > 1 ? ( @@ -56,23 +44,7 @@ export function UsageControls({ /> ) : null} {view.kind === "unavailable" ? null : ( - <> - - {/* The Changes panel's refresh: an icon, with the label in its tooltip. */} - - {busy ? ( - - ) : ( - - )} - - + )} ); diff --git a/packages/app/src/usage/copy.ts b/packages/app/src/usage/copy.ts index 5d84805ea2..731450f30d 100644 --- a/packages/app/src/usage/copy.ts +++ b/packages/app/src/usage/copy.ts @@ -3,6 +3,7 @@ export const usageCopy = { title: "Usage", planUsage: "Plan usage", + options: "Usage options", refresh: "Refresh", refreshing: "Refreshing...", refreshFailed: "Unable to refresh usage", @@ -16,6 +17,8 @@ export const usageCopy = { clientUnavailable: "Host connection is not ready", retry: "Try again", pin: "Pin", + displayAs: "Show", displayUsed: "Used", displayRemaining: "Remaining", + showInSidebar: "Show in sidebar", } as const; diff --git a/packages/app/src/usage/display-toggle.tsx b/packages/app/src/usage/display-toggle.tsx deleted file mode 100644 index f778172b7c..0000000000 --- a/packages/app/src/usage/display-toggle.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { SegmentedControl, type SegmentedControlOption } from "@/components/ui/segmented-control"; -import { usageCopy } from "./copy"; -import type { UsageDisplay } from "./display"; -import type { UsageDisplayAs } from "./preferences"; - -const OPTIONS: SegmentedControlOption[] = [ - { value: "used", label: usageCopy.displayUsed, testID: "usage-display-used" }, - { value: "remaining", label: usageCopy.displayRemaining, testID: "usage-display-remaining" }, -]; - -/** Switches every usage surface between the share used and the share left. */ -export function UsageDisplayToggle({ display }: { display: UsageDisplay }) { - return ( - - ); -} diff --git a/packages/app/src/usage/format.test.ts b/packages/app/src/usage/format.test.ts new file mode 100644 index 0000000000..88fe227ae6 --- /dev/null +++ b/packages/app/src/usage/format.test.ts @@ -0,0 +1,15 @@ +import { describe, expect, it } from "vitest"; +import { formatAmount } from "./format"; + +describe("formatAmount", () => { + it("groups thousands in the app's language", () => { + expect(formatAmount(12345, "credits", "en")).toBe("12,345"); + expect(formatAmount(12345, "requests", "en")).toBe("12,345"); + expect(formatAmount(12345, "credits", "fr").replace(/\s/g, " ")).toBe("12 345"); + }); + + it("formats dollars as the language writes currency", () => { + expect(formatAmount(1234.5, "usd", "en")).toBe("$1,234.50"); + expect(formatAmount(1234.5, "usd", "fr").replace(/\s/g, " ")).toBe("1 234,50 $US"); + }); +}); diff --git a/packages/app/src/usage/format.ts b/packages/app/src/usage/format.ts index 253586c173..5adef2154d 100644 --- a/packages/app/src/usage/format.ts +++ b/packages/app/src/usage/format.ts @@ -34,13 +34,14 @@ export function formatResetLabel(iso: string | null | undefined): string | null return rel === "now" ? "resetting now" : `resets ${rel}`; } -export function formatAmount(value: number, unit: UsageBalanceUnit): string { +/** A balance amount as the app's language writes it: "$1,234.50", "12,345". */ +export function formatAmount(value: number, unit: UsageBalanceUnit, locale: string): string { switch (unit) { case "usd": - return `$${value.toFixed(2)}`; + return new Intl.NumberFormat(locale, { style: "currency", currency: "USD" }).format(value); case "tokens": return formatTokenCount(value); default: - return value.toLocaleString(); + return new Intl.NumberFormat(locale, { maximumFractionDigits: 2 }).format(value); } } diff --git a/packages/app/src/usage/index.ts b/packages/app/src/usage/index.ts index 6ab6c1447d..c94fbb01f9 100644 --- a/packages/app/src/usage/index.ts +++ b/packages/app/src/usage/index.ts @@ -1,3 +1,3 @@ export { HostUsageSection } from "./host-usage-section"; export { UsageScreen } from "./usage-screen"; -export { UsageSidebarItem } from "./sidebar-item"; +export { UsageSidebarItem, useOpenUsageScreen } from "./sidebar-item"; diff --git a/packages/app/src/usage/meter.tsx b/packages/app/src/usage/meter.tsx new file mode 100644 index 0000000000..37842cafb5 --- /dev/null +++ b/packages/app/src/usage/meter.tsx @@ -0,0 +1,74 @@ +import { useMemo } from "react"; +import { View, type StyleProp, type ViewStyle } from "react-native"; +import { StyleSheet } from "react-native-unistyles"; +import { clampPct } from "./format"; +import type { UsageTone } from "./types"; + +function fillToneStyle(tone: UsageTone) { + switch (tone) { + case "ok": + return styles.fillOk; + case "warning": + return styles.fillWarning; + case "danger": + return styles.fillDanger; + default: + return styles.fillDefault; + } +} + +/** + * A window's share as a bar, coloured by its tone. `quiet` fills it with a surface colour instead, + * for summaries where a row of coloured bars would be noise. The caller sizes the track. + */ +export function UsageMeter({ + percent, + tone, + quiet = false, + style, +}: { + percent: number; + tone: UsageTone; + quiet?: boolean; + style?: StyleProp; +}) { + const fillWidth = clampPct(percent); + const fillStyle = useMemo>( + () => [styles.fill, quiet ? styles.fillQuiet : fillToneStyle(tone), { width: `${fillWidth}%` }], + [fillWidth, quiet, tone], + ); + return ( + + + + ); +} + +const styles = StyleSheet.create((theme) => ({ + track: { + height: 4, + borderRadius: 2, + backgroundColor: theme.colors.surface3, + overflow: "hidden", + }, + // Follows the track, so a caller that sizes the track sizes the bar. + fill: { + height: "100%", + borderRadius: theme.borderRadius.full, + }, + fillQuiet: { + backgroundColor: theme.colors.surface4, + }, + fillDefault: { + backgroundColor: theme.colors.foregroundMuted, + }, + fillOk: { + backgroundColor: theme.colors.statusSuccess, + }, + fillWarning: { + backgroundColor: theme.colors.statusWarning, + }, + fillDanger: { + backgroundColor: theme.colors.statusDanger, + }, +})); diff --git a/packages/app/src/usage/options-menu.tsx b/packages/app/src/usage/options-menu.tsx new file mode 100644 index 0000000000..05184d5d5a --- /dev/null +++ b/packages/app/src/usage/options-menu.tsx @@ -0,0 +1,103 @@ +import { Settings } from "lucide-react-native"; +import { useCallback } from "react"; +import { withUnistyles } from "react-native-unistyles"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, +} from "@/components/ui/dropdown-menu"; +import { extraMutedIconColorMapping } from "@/components/ui/icon-button-chrome"; +import { LoadingSpinner } from "@/components/ui/loading-spinner"; +import { ToolbarButton, paneContentToolbarIconSize } from "@/components/ui/pane-content-toolbar"; +import { useIsCompactFormFactor } from "@/constants/layout"; +import { useSidebarNavItems } from "@/sidebar-nav/use-sidebar-nav-items"; +import { usageCopy } from "./copy"; +import type { UsageDisplay } from "./display"; + +const ThemedSettings = withUnistyles(Settings); +const ThemedLoadingSpinner = withUnistyles(LoadingSpinner); + +/** + * The usage title row's options: Refresh every report, whether percents read as used or + * remaining, and whether the sidebar shows the Usage item (the same switch as Settings > Sidebar). The trigger spins while a refresh is in flight, since the menu hides the one it came + * from. + */ +export function UsageOptionsMenu({ + display, + busy, + onRefresh, +}: { + display: UsageDisplay; + busy: boolean; + onRefresh: () => void; +}) { + const compact = useIsCompactFormFactor(); + const iconSize = paneContentToolbarIconSize(compact); + const { displayAs, setDisplayAs } = display; + const showUsed = useCallback(() => setDisplayAs("used"), [setDisplayAs]); + const showRemaining = useCallback(() => setDisplayAs("remaining"), [setDisplayAs]); + const sidebarItems = useSidebarNavItems("footer"); + const inSidebar = sidebarItems.items.some((item) => item.key === "usage" && item.visible); + const { setVisible } = sidebarItems; + const toggleInSidebar = useCallback( + () => setVisible("usage", !inSidebar), + [inSidebar, setVisible], + ); + return ( + + + {busy ? ( + + ) : ( + + )} + + + + {usageCopy.refresh} + + + {usageCopy.displayAs} + + {usageCopy.displayUsed} + + + {usageCopy.displayRemaining} + + + + {usageCopy.showInSidebar} + + + + ); +} diff --git a/packages/app/src/usage/pinned.test.ts b/packages/app/src/usage/pinned.test.ts index af6d48cb12..24ba59be7a 100644 --- a/packages/app/src/usage/pinned.test.ts +++ b/packages/app/src/usage/pinned.test.ts @@ -1,6 +1,11 @@ import { describe, expect, it } from "vitest"; import type { UsagePreferences } from "./preferences"; -import { resolvePinnedUsage } from "./pinned"; +import { + choosePinnedUsageLayout, + resolvePinnedUsage, + type PinnedUsageSource, + type PinnedUsageWindow, +} from "./pinned"; import type { UsageReportEntry, UsageWindow } from "./types"; function report(input: { @@ -26,8 +31,8 @@ const claude = report({ sourceLabel: "Claude", icon: "", windows: [ - { id: "five-hour", label: "5-hour", usedPct: 31 }, - { id: "weekly", label: "Weekly", usedPct: 80 }, + { id: "five-hour", label: "Session", shortLabel: "5h", usedPct: 31 }, + { id: "weekly", label: "Weekly", shortLabel: "wk", usedPct: 80 }, ], }); const codex = report({ @@ -36,6 +41,10 @@ const codex = report({ windows: [{ id: "weekly", label: "Weekly", remainingPct: 88 }], }); +function windows(reports: UsageReportEntry[], prefs: UsagePreferences): PinnedUsageWindow[] { + return resolvePinnedUsage(reports, prefs).flatMap((source) => source.windows); +} + function preferences( pinned: UsagePreferences["pinned"], displayAs: UsagePreferences["displayAs"] = "used", @@ -43,6 +52,26 @@ function preferences( return { displayAs, pinned, serverId: null }; } +describe("choosePinnedUsageLayout", () => { + const widths = { labelsWidth: 200, percentsWidth: 120, windowCount: 2 }; + + it("shows meters only when every window's meter fits beside the labels", () => { + expect(choosePinnedUsageLayout({ ...widths, available: 240 })).toBe("meters"); + expect(choosePinnedUsageLayout({ ...widths, available: 239 })).toBe("labels"); + }); + + it("drops the labels before it lets the line overflow", () => { + expect(choosePinnedUsageLayout({ ...widths, available: 199 })).toBe("percents"); + }); + + it("shows labels until the widths are measured", () => { + expect(choosePinnedUsageLayout({ ...widths, available: null })).toBe("labels"); + expect(choosePinnedUsageLayout({ ...widths, available: 999, labelsWidth: null })).toBe( + "labels", + ); + }); +}); + describe("resolvePinnedUsage", () => { it("defaults to the first window with a percent for each source account", () => { const work = report({ @@ -60,57 +89,113 @@ describe("resolvePinnedUsage", () => { windows: [{ id: "empty", label: "Empty" }], }); expect( - resolvePinnedUsage([claude, codex, work, empty], preferences([])).map((item) => [ + windows([claude, codex, work, empty], preferences([])).map((item) => [ item.key, item.percentText, ]), ).toEqual([ ["claude:default/five-hour", "31%"], - ["codex:default/weekly", "12%"], ["claude:work/weekly", "90%"], + ["codex:default/weekly", "12%"], ]); }); it("pins replace defaults, and removing all pins restores defaults", () => { const reports = [claude, codex]; expect( - resolvePinnedUsage(reports, preferences([{ sourceId: "claude", windowId: "weekly" }])).map( + windows(reports, preferences([{ sourceId: "claude", windowId: "weekly" }])).map( (item) => item.key, ), ).toEqual(["claude:default/weekly"]); - expect(resolvePinnedUsage(reports, preferences([])).map((item) => item.key)).toEqual([ + expect(windows(reports, preferences([])).map((item) => item.key)).toEqual([ "claude:default/five-hour", "codex:default/weekly", ]); }); - it("shows pinned windows in pin order, not report order", () => { - const items = resolvePinnedUsage( - [claude, codex], + it("groups pinned windows by source in report order, not pin order", () => { + const work = report({ + sourceId: "claude", + sourceLabel: "Claude", + account: "work", + windows: [{ id: "weekly", label: "Weekly", usedPct: 90 }], + }); + const sources = resolvePinnedUsage( + [claude, codex, work], preferences([ + { sourceId: "claude", windowId: "weekly" }, { sourceId: "codex", windowId: "weekly" }, { sourceId: "claude", windowId: "five-hour" }, ]), ); - expect(items).toEqual([ - { - key: "codex:default/weekly", - icon: null, - label: "Codex Weekly 12% used", - percentText: "12%", - }, - { - key: "claude:default/five-hour", - icon: "", - label: "Claude 5-hour 31% used", - percentText: "31%", - }, + const windowKeys = (source: PinnedUsageSource) => source.windows.map((window) => window.key); + expect(sources.map((source) => [source.key, windowKeys(source)])).toEqual([ + ["claude:default", ["claude:default/five-hour", "claude:default/weekly"]], + ["claude:work", ["claude:work/weekly"]], + ["codex:default", ["codex:default/weekly"]], + ]); + expect(sources[0]).toEqual({ + key: "claude:default", + icon: "", + windows: [ + { + key: "claude:default/five-hour", + label: "Claude Session 31% used", + shortLabel: "5h", + percent: 31, + percentText: "31%", + tone: "default", + }, + { + key: "claude:default/weekly", + label: "Claude Weekly 80% used", + shortLabel: "wk", + percent: 80, + percentText: "80%", + tone: "warning", + }, + ], + }); + }); + + it("names a window by its label when the source gives no short label", () => { + const items = windows([codex], preferences([{ sourceId: "codex", windowId: "weekly" }])); + + expect(items.map((item) => item.shortLabel)).toEqual(["Weekly"]); + }); + + it("defaults to every window the source marks as summary, when it marks any", () => { + const marked = report({ + sourceId: "claude", + sourceLabel: "Claude", + windows: [ + { id: "five-hour", label: "Session", shortLabel: "5h", usedPct: 31, summary: true }, + { id: "weekly", label: "Weekly", shortLabel: "wk", usedPct: 80, summary: true }, + { id: "weekly-fable", label: "Weekly · Fable", usedPct: 8 }, + ], + }); + + expect(windows([marked, codex], preferences([])).map((item) => item.key)).toEqual([ + "claude:default/five-hour", + "claude:default/weekly", + "codex:default/weekly", ]); }); + it("keeps an empty short label empty, for a percent that needs no name", () => { + const opencode = report({ + sourceId: "opencode", + sourceLabel: "OpenCode Go", + windows: [{ id: "rolling", label: "Rolling", shortLabel: "", usedPct: 21 }], + }); + const items = windows([opencode], preferences([])); + + expect(items.map((item) => item.shortLabel)).toEqual([""]); + }); + it("formats the share left when the user reads usage as remaining", () => { - const items = resolvePinnedUsage( + const items = windows( [claude, codex], preferences( [ @@ -123,7 +208,7 @@ describe("resolvePinnedUsage", () => { expect(items.map((item) => item.percentText)).toEqual(["69%", "88%"]); expect(items.map((item) => item.label)).toEqual([ - "Claude 5-hour 69% left", + "Claude Session 69% left", "Codex Weekly 88% left", ]); }); @@ -134,7 +219,7 @@ describe("resolvePinnedUsage", () => { sourceLabel: "OpenCode", windows: [{ id: "monthly", label: "Monthly" }], }); - const items = resolvePinnedUsage( + const items = windows( [claude, noPercent], preferences([ { sourceId: "claude", windowId: "monthly" }, @@ -159,7 +244,7 @@ describe("resolvePinnedUsage", () => { account: "work", windows: [{ id: "five-hour", label: "5-hour", usedPct: 90 }], }); - const items = resolvePinnedUsage( + const items = windows( [personal, codex, work], preferences([{ sourceId: "claude", windowId: "five-hour" }]), ); diff --git a/packages/app/src/usage/pinned.ts b/packages/app/src/usage/pinned.ts index 11cf0ecabf..003d56d4e3 100644 --- a/packages/app/src/usage/pinned.ts +++ b/packages/app/src/usage/pinned.ts @@ -1,15 +1,53 @@ import { formatPct } from "./format"; import { displayPercent } from "./model"; import type { UsagePreferences } from "./preferences"; -import type { UsageReportEntry } from "./types"; +import { windowTone } from "./tone"; +import type { UsageReportEntry, UsageTone, UsageWindow } from "./types"; /** One summary window of one account, as the sidebar Usage item shows it. */ export interface PinnedUsageWindow { key: string; - icon: string | null; /** Accessible description, including the source, account, window and percent meaning. */ label: string; + /** The window's short name ("5h"), empty for none, or its label when the source sends none. */ + shortLabel: string; + /** The share shown, under the user's used/remaining preference. */ + percent: number; percentText: string; + tone: UsageTone; +} + +/** One account's summary windows, under its source's icon. */ +export interface PinnedUsageSource { + key: string; + icon: string | null; + windows: PinnedUsageWindow[]; +} + +/** How much of each window the one-line summary has room for. */ +export type PinnedUsageLayout = "meters" | "labels" | "percents"; + +// A meter narrower than this is a sliver that reads as noise rather than a share. +export const MIN_METER_WIDTH = 16; +/** The space between a meter and its text. */ +export const METER_GAP = 4; + +/** + * The richest layout that fits on one line: every window's meter, else its short label, else its + * percent alone. Meters are all or none; a line never mixes them. Until the widths are known it + * shows labels, which never flashes meters that turn out not to fit. + */ +export function choosePinnedUsageLayout(input: { + available: number | null; + labelsWidth: number | null; + percentsWidth: number | null; + windowCount: number; +}): PinnedUsageLayout { + const { available, labelsWidth, percentsWidth, windowCount } = input; + if (available === null || labelsWidth === null || percentsWidth === null) return "labels"; + if (labelsWidth + windowCount * (MIN_METER_WIDTH + METER_GAP) <= available) return "meters"; + if (labelsWidth <= available) return "labels"; + return percentsWidth < labelsWidth ? "percents" : "labels"; } function describe(entry: UsageReportEntry, windowLabel: string): string { @@ -17,40 +55,52 @@ function describe(entry: UsageReportEntry, windowLabel: string): string { return `${entry.sourceLabel}${account} ${windowLabel}`; } -/** Pins replace defaults; without pins, summarize each account's first window with a percent. */ +/** Reports with every account of a source together, sources in the order they first appear. */ +function groupBySource(reports: readonly UsageReportEntry[]): UsageReportEntry[] { + const sourceIds = [...new Set(reports.map((entry) => entry.sourceId))]; + return sourceIds.flatMap((sourceId) => reports.filter((entry) => entry.sourceId === sourceId)); +} + +/** + * Pins replace defaults. Without pins, an account shows the windows its source marks as summary, + * or its first window with a percent when the source marks none. + */ +function summaryWindows(entry: UsageReportEntry, preferences: UsagePreferences): UsageWindow[] { + const withPercent = entry.report.windows.filter( + (window) => displayPercent(window, preferences.displayAs) !== null, + ); + if (preferences.pinned.length === 0) { + const marked = withPercent.filter((window) => window.summary); + return marked.length > 0 ? marked : withPercent.slice(0, 1); + } + return withPercent.filter((window) => + preferences.pinned.some((pin) => pin.sourceId === entry.sourceId && pin.windowId === window.id), + ); +} + +/** + * The sidebar Usage item's summary: one group per account, grouped by source, each with its + * windows in its report's order. Accounts with nothing to show are left out. + */ export function resolvePinnedUsage( reports: readonly UsageReportEntry[], preferences: UsagePreferences, -): PinnedUsageWindow[] { - const selections = - preferences.pinned.length === 0 - ? reports.map((entry) => ({ - entry, - window: entry.report.windows.find( - (window) => displayPercent(window, preferences.displayAs) !== null, - ), - })) - : preferences.pinned.flatMap((pin) => - reports - .filter((entry) => entry.sourceId === pin.sourceId) - .map((entry) => ({ - entry, - window: entry.report.windows.find((window) => window.id === pin.windowId), - })), - ); - return selections.flatMap(({ entry, window }) => { - if (!window) return []; - const percent = displayPercent(window, preferences.displayAs); - if (percent === null) return []; - const percentText = formatPct(percent); - const meaning = preferences.displayAs === "remaining" ? "left" : "used"; - return [ - { +): PinnedUsageSource[] { + const meaning = preferences.displayAs === "remaining" ? "left" : "used"; + return groupBySource(reports).flatMap((entry) => { + const windows = summaryWindows(entry, preferences).map((window) => { + const percent = displayPercent(window, preferences.displayAs) ?? 0; + const percentText = formatPct(percent); + return { key: `${entry.id}/${window.id}`, - icon: entry.icon ?? null, label: `${describe(entry, window.label)} ${percentText} ${meaning}`, + shortLabel: window.shortLabel ?? window.label, + percent, percentText, - }, - ]; + tone: windowTone(window), + }; + }); + if (windows.length === 0) return []; + return [{ key: entry.id, icon: entry.icon ?? null, windows }]; }); } diff --git a/packages/app/src/usage/sidebar-item.tsx b/packages/app/src/usage/sidebar-item.tsx index 9b9e9a80ef..ee8f6f657b 100644 --- a/packages/app/src/usage/sidebar-item.tsx +++ b/packages/app/src/usage/sidebar-item.tsx @@ -1,8 +1,14 @@ import { router } from "expo-router"; -import { Gauge } from "lucide-react-native"; -import { useCallback, useMemo, useState } from "react"; +import { CircleGauge } from "lucide-react-native"; +import { Fragment, useCallback, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; -import { Pressable, Text, View, type PressableStateCallbackType } from "react-native"; +import { + Pressable, + Text, + View, + type LayoutChangeEvent, + type PressableStateCallbackType, +} from "react-native"; import { StyleSheet } from "react-native-unistyles"; import { SidebarHeaderRow } from "@/components/sidebar/sidebar-header-row"; import { SidebarPopoverRoot, SidebarPopoverSurface } from "@/components/sidebar/sidebar-popover"; @@ -16,13 +22,21 @@ import { useUsageHostId, useUsageHostSelection } from "./hosts"; import type { UsagePreferences } from "./preferences"; import { useHostUsage } from "./queries"; import { UsageSourceIcon } from "./source-icon"; -import { resolvePinnedUsage, type PinnedUsageWindow } from "./pinned"; +import { UsageMeter } from "./meter"; +import { + choosePinnedUsageLayout, + METER_GAP, + MIN_METER_WIDTH, + resolvePinnedUsage, + type PinnedUsageLayout, + type PinnedUsageSource, +} from "./pinned"; import type { UsageReportEntry } from "./types"; import type { UsageHost } from "./model"; import { UsageBody } from "./usage-section"; const NO_REPORTS: UsageReportEntry[] = []; -const NO_ITEMS: PinnedUsageWindow[] = []; +const NO_SOURCES: PinnedUsageSource[] = []; /** * The sidebar footer's usage entry: each summary window's source icon and percent, or a plain @@ -33,7 +47,7 @@ export function UsageSidebarItem() { const { preferences, display } = useUsagePreferences(); const serverId = useUsageHostId(); if (!serverId) { - return ; + return ; } return ( resolvePinnedUsage(reports, preferences), [preferences, reports]); - return ; + const sources = useMemo(() => resolvePinnedUsage(reports, preferences), [preferences, reports]); + return ; } -function useOpenUsageScreen(): () => void { +/** Opens the Usage screen, over the sidebar on compact layouts. */ +export function useOpenUsageScreen(): () => void { const isCompact = useIsCompactFormFactor(); const showMobileAgent = usePanelStore((state) => state.showMobileAgent); return useCallback(() => { @@ -71,11 +86,11 @@ function useOpenUsageScreen(): () => void { function UsageEntry({ serverId, - items, + sources, display, }: { serverId: string | null; - items: readonly PinnedUsageWindow[]; + sources: readonly PinnedUsageSource[]; display: UsageDisplay; }) { const { t } = useTranslation(); @@ -97,12 +112,12 @@ function UsageEntry({ }, [openUsageScreen, usesSheet]); const trigger = - items.length > 0 ? ( - + sources.length > 0 ? ( + ) : ( item.label).join(", ")}`; +function pinnedUsageLabel(label: string, sources: readonly PinnedUsageSource[]): string { + const windows = sources.flatMap((source) => source.windows); + return `${label}: ${windows.map((window) => window.label).join(", ")}`; } function triggerStyle({ hovered }: PressableStateCallbackType & { hovered?: boolean }) { return hovered ? [styles.trigger, styles.triggerHovered] : styles.trigger; } +function layoutWidth(event: LayoutChangeEvent): number { + return event.nativeEvent.layout.width; +} + +/** + * One line: each account's icon, then per window a meter and "31% 5h". Off-screen copies measure + * the line with labels and with percents alone; the richest layout that fits is shown, and what + * it leaves out takes no space at all. + */ function PinnedUsageTrigger({ label, - items, + sources, onPress, }: { label: string; - items: readonly PinnedUsageWindow[]; + sources: readonly PinnedUsageSource[]; onPress: () => void; }) { + const [available, setAvailable] = useState(null); + const [labelsWidth, setLabelsWidth] = useState(null); + const [percentsWidth, setPercentsWidth] = useState(null); + const handleAvailableLayout = useCallback((event: LayoutChangeEvent) => { + setAvailable(layoutWidth(event)); + }, []); + const handleLabelsLayout = useCallback((event: LayoutChangeEvent) => { + setLabelsWidth(layoutWidth(event)); + }, []); + const handlePercentsLayout = useCallback((event: LayoutChangeEvent) => { + setPercentsWidth(layoutWidth(event)); + }, []); + const layout = choosePinnedUsageLayout({ + available, + labelsWidth, + percentsWidth, + windowCount: sources.reduce((count, source) => count + source.windows.length, 0), + }); return ( - {items.map((item) => ( - - - - {item.percentText} - + + + + + + - ))} + + + + ); } +/** + * The line's items as direct children of one row, so every meter shares the leftover width + * equally. + */ +function PinnedUsageLine({ + sources, + layout, + measuring = false, +}: { + sources: readonly PinnedUsageSource[]; + layout: PinnedUsageLayout; + measuring?: boolean; +}) { + return sources.map((source, sourceIndex) => ( + + + + + {source.windows.map((window, windowIndex) => { + const leadStyle = windowIndex === 0 ? styles.iconGap : styles.windowGap; + return ( + + {layout === "meters" ? ( + + + + ) : null} + + {window.percentText} + {layout === "percents" || window.shortLabel === "" ? null : ( + {` ${window.shortLabel}`} + )} + + + ); + })} + + )); +} + const styles = StyleSheet.create((theme) => ({ trigger: { width: "100%", flexDirection: "row", - flexWrap: "wrap", alignItems: "center", - // A wrapping row packs its lines at the top; center them in the 28px row instead. - alignContent: "center", - columnGap: theme.spacing[3], - rowGap: theme.spacing[1], + overflow: "hidden", // Same row geometry and leading rail as Add project and the footer icons. minHeight: 28, paddingVertical: theme.spacing[1], - paddingHorizontal: theme.spacing[1.5], + paddingHorizontal: theme.spacing[2], borderRadius: theme.borderRadius.lg, }, triggerHovered: { backgroundColor: theme.colors.surfaceSidebarHover, }, - item: { + line: { + flex: 1, + minWidth: 0, flexDirection: "row", alignItems: "center", - gap: theme.spacing[1], + overflow: "hidden", + }, + // Wide enough that nothing inside wraps or shrinks; each child reports its natural width. + measure: { + position: "absolute", + top: 0, + left: 0, + width: 10_000, + opacity: 0, + }, + measured: { + alignSelf: "flex-start", + flexDirection: "row", + alignItems: "center", + }, + sourceGap: { + marginLeft: theme.spacing[3], + }, + iconGap: { + marginLeft: theme.spacing[1.5], + }, + windowGap: { + marginLeft: theme.spacing[2], + }, + // Every slot takes an equal share of the leftover width, so the line fills the row. + meterSlot: { + flexGrow: 1, + flexShrink: 1, + flexBasis: 0, + minWidth: MIN_METER_WIDTH, + marginRight: METER_GAP, + }, + meter: { + height: 6, + borderRadius: 3, }, percent: { - color: theme.colors.foregroundMuted, + flexShrink: 0, + color: theme.colors.foreground, fontSize: theme.fontSize.sm, fontVariant: ["tabular-nums"], }, + windowLabel: { + color: theme.colors.foregroundMuted, + }, sheetBody: { padding: theme.spacing[3], gap: theme.spacing[3], diff --git a/packages/app/src/usage/tone.ts b/packages/app/src/usage/tone.ts index 4727853c2f..d35d836f8b 100644 --- a/packages/app/src/usage/tone.ts +++ b/packages/app/src/usage/tone.ts @@ -1,6 +1,10 @@ -import type { UsageTone } from "./types"; +import { usedPercent } from "./model"; +import type { UsageTone, UsageWindow } from "./types"; -export function deriveTone(usedPct: number | null | undefined): UsageTone { +/** The source's tone for a window, or one derived from how much of it is used. */ +export function windowTone(window: UsageWindow): UsageTone { + if (window.tone) return window.tone; + const usedPct = usedPercent(window); if (usedPct == null) return "default"; if (usedPct > 90) return "danger"; if (usedPct >= 70) return "warning"; diff --git a/packages/app/src/usage/window-bar.tsx b/packages/app/src/usage/window-bar.tsx index ce6e43b5e6..4412836cc4 100644 --- a/packages/app/src/usage/window-bar.tsx +++ b/packages/app/src/usage/window-bar.tsx @@ -1,25 +1,13 @@ import { useMemo } from "react"; import { Pressable, Text, View, type StyleProp, type ViewStyle } from "react-native"; import { StyleSheet } from "react-native-unistyles"; -import { clampPct, formatDisplayPct, formatResetLabel } from "./format"; -import { displayPercent, usageWindowRowLabel, usedPercent } from "./model"; +import { formatDisplayPct, formatResetLabel } from "./format"; +import { UsageMeter } from "./meter"; +import { displayPercent, usageWindowRowLabel } from "./model"; import type { UsageDisplayAs } from "./preferences"; -import { deriveTone } from "./tone"; +import { windowTone } from "./tone"; import type { UsageTone, UsageWindow } from "./types"; -function fillToneStyle(tone: UsageTone) { - switch (tone) { - case "ok": - return styles.fillOk; - case "warning": - return styles.fillWarning; - case "danger": - return styles.fillDanger; - default: - return styles.fillDefault; - } -} - // Pinned rows carry the pinned surface; hovering an unpinned row previews it at half strength, // so a hover never reads as the selection. Pinned rows do not react to hover. function highlightStyle(pinned: boolean, hovered: boolean) { @@ -43,15 +31,8 @@ export function UsageWindowBar({ pinLabel: string; pinTestID: string; }) { - const usedPct = usedPercent(window); const shownPct = displayPercent(window, displayAs); - const tone = window.tone ?? deriveTone(usedPct); - - const fillWidth = clampPct(shownPct ?? 0); - const fillStyle = useMemo>( - () => [styles.fill, fillToneStyle(tone), { width: `${fillWidth}%` }], - [fillWidth, tone], - ); + const tone = windowTone(window); const isAtRisk = window.runsOutAt != null && window.shortfallPct != null; const trailing = isAtRisk @@ -80,7 +61,8 @@ export function UsageWindowBar({ value={value} trailing={trailing} isAtRisk={isAtRisk} - fillStyle={fillStyle} + percent={shownPct ?? 0} + tone={tone} /> )} @@ -93,14 +75,16 @@ function WindowRowContent({ value, trailing, isAtRisk, - fillStyle, + percent, + tone, }: { highlight: StyleProp; label: string; value: string; trailing: string | null | undefined; isAtRisk: boolean; - fillStyle: StyleProp; + percent: number; + tone: UsageTone; }) { return ( <> @@ -116,9 +100,7 @@ function WindowRowContent({ ) : null} - - - + ); } @@ -174,26 +156,4 @@ const styles = StyleSheet.create((theme) => ({ color: theme.colors.statusDanger, fontWeight: theme.fontWeight.normal, }, - track: { - height: 4, - borderRadius: 2, - backgroundColor: theme.colors.surface3, - overflow: "hidden", - }, - fill: { - height: 4, - borderRadius: 2, - }, - fillDefault: { - backgroundColor: theme.colors.foregroundMuted, - }, - fillOk: { - backgroundColor: theme.colors.statusSuccess, - }, - fillWarning: { - backgroundColor: theme.colors.statusWarning, - }, - fillDanger: { - backgroundColor: theme.colors.statusDanger, - }, })); diff --git a/packages/app/test-stubs/gorhom-bottom-sheet.ts b/packages/app/test-stubs/gorhom-bottom-sheet.ts index 0ab1cd7215..784a7569ff 100644 --- a/packages/app/test-stubs/gorhom-bottom-sheet.ts +++ b/packages/app/test-stubs/gorhom-bottom-sheet.ts @@ -12,6 +12,10 @@ export const BottomSheetScrollView = PassThrough; export const BottomSheetView = PassThrough; export const BottomSheetTextInput = Stub; +export function useBottomSheetModal(): never { + throw new Error("Modal stack interactions require the real Gorhom provider in browser E2E tests"); +} + // The app imports the internal layout values to compute the visible sheet body. Browser unit tests // never render the native sheet, but Vite still validates this module's export surface. const sharedValue = (value: unknown) => ({ get: () => value }); diff --git a/packages/plugin/src/server/usage.ts b/packages/plugin/src/server/usage.ts index ed49db0f1b..11906dd955 100644 --- a/packages/plugin/src/server/usage.ts +++ b/packages/plugin/src/server/usage.ts @@ -5,6 +5,13 @@ import type { JsonValue } from "@getpaseo/protocol/agent-types"; export interface UsageWindow { id: string; label: string; + /** + * A few characters naming the window where space is tight, e.g. "5h" or "wk". An empty string + * shows the percent alone; leaving it out shows `label`. + */ + shortLabel?: string; + /** Shown in the usage summary until the user pins windows of their own. */ + summary?: boolean; usedPct?: number | null; remainingPct?: number | null; resetsAt?: string | null; @@ -54,6 +61,8 @@ export interface UsageSourceRegistration { export function windowFromUsedPct(input: { id: string; label: string; + shortLabel?: string; + summary?: boolean; utilizationPct: number | null | undefined; resetsAt?: string | null; tone?: UsageWindow["tone"]; @@ -66,6 +75,8 @@ export function windowFromUsedPct(input: { remainingPct: usedPct === null ? null : Math.max(0, 100 - usedPct), resetsAt: input.resetsAt ?? null, }; + if (input.shortLabel !== undefined) window.shortLabel = input.shortLabel; + if (input.summary) window.summary = true; if (input.tone) window.tone = input.tone; return window; } diff --git a/packages/protocol/src/messages.ts b/packages/protocol/src/messages.ts index 69595c9ad6..861ddf4a37 100644 --- a/packages/protocol/src/messages.ts +++ b/packages/protocol/src/messages.ts @@ -6189,6 +6189,13 @@ export const ProviderUsageStatusSchema = z.enum(["available", "unavailable", "er export const ProviderUsageWindowSchema = z.object({ id: z.string(), label: z.string(), + /** + * A few characters naming the window where space is tight, e.g. "5h" or "wk". An empty string + * shows the percent alone; leaving it out shows `label`. + */ + shortLabel: z.string().optional(), + /** Shown in the usage summary until the user pins windows of their own. */ + summary: z.boolean().optional(), usedPct: z.number().nullable().optional(), remainingPct: z.number().nullable().optional(), resetsAt: z.string().nullable().optional(), diff --git a/plugins/claude-usage-source/server/usage.test.ts b/plugins/claude-usage-source/server/usage.test.ts index d20162e93d..b9eb4b6ecf 100644 --- a/plugins/claude-usage-source/server/usage.test.ts +++ b/plugins/claude-usage-source/server/usage.test.ts @@ -307,6 +307,38 @@ describe("Claude usage source scoped weekly limits", () => { ); }); + it("summarizes the session and weekly windows by default, not scoped ones", async () => { + const { provider } = claudeProvider({ + five_hour: { utilization: 6, resets_at: "2026-06-01T21:00:00Z" }, + seven_day: { utilization: 23, resets_at: "2026-06-04T00:00:00Z" }, + limits: [fableLimit()], + }); + + const usage = await provider.fetchUsage(); + + expect(usage.windows.map((window) => [window.id, window.summary ?? false])).toEqual([ + ["five_hour", true], + ["weekly", true], + ["weekly_model_fable", false], + ]); + }); + + it("gives every window a short label for tight spaces", async () => { + const { provider } = claudeProvider({ + five_hour: { utilization: 6, resets_at: "2026-06-01T21:00:00Z" }, + seven_day: { utilization: 23, resets_at: "2026-06-04T00:00:00Z" }, + limits: [fableLimit()], + }); + + const usage = await provider.fetchUsage(); + + expect(usage.windows.map((window) => [window.id, window.shortLabel])).toEqual([ + ["five_hour", "5h"], + ["weekly", "wk"], + ["weekly_model_fable", "wk Fable"], + ]); + }); + it("renders a scoped window that is at zero and inactive", async () => { const { provider } = claudeProvider({ seven_day: { utilization: 23, resets_at: "2026-06-04T00:00:00Z" }, diff --git a/plugins/claude-usage-source/server/usage.ts b/plugins/claude-usage-source/server/usage.ts index 9e280df0e5..70ba4f9273 100644 --- a/plugins/claude-usage-source/server/usage.ts +++ b/plugins/claude-usage-source/server/usage.ts @@ -112,9 +112,11 @@ const UNSCOPED_WINDOWS: ReadonlyArray<{ field: "five_hour" | "seven_day"; id: string; label: string; + shortLabel: string; + summary: boolean; }> = [ - { field: "five_hour", id: "five_hour", label: "Session" }, - { field: "seven_day", id: "weekly", label: "Weekly" }, + { field: "five_hour", id: "five_hour", label: "Session", shortLabel: "5h", summary: true }, + { field: "seven_day", id: "weekly", label: "Weekly", shortLabel: "wk", summary: true }, ]; // Scoped windows from before `limits[]` existed. Declaring the dimension here is what @@ -253,6 +255,8 @@ function unscopedWindows(resp: ClaudeUsageResponse): UsageWindow[] { windowFromUsedPct({ id: spec.id, label: spec.label, + shortLabel: spec.shortLabel, + summary: spec.summary, utilizationPct: window.utilization, resetsAt: window.resets_at ?? null, tone: toneFromUsedPct(window.utilization), @@ -272,6 +276,7 @@ function scopedWindows(limits: ScopedLimit[]): UsageWindow[] { return windowFromUsedPct({ id, label: `Weekly \u00b7 ${limit.name}`, + shortLabel: `wk ${limit.name}`, utilizationPct: limit.usedPct, resetsAt: limit.resetsAt, tone: toneFromUsedPct(limit.usedPct), diff --git a/plugins/codex-usage-source/server/usage.test.ts b/plugins/codex-usage-source/server/usage.test.ts index dfa58ef603..e7572f4efe 100644 --- a/plugins/codex-usage-source/server/usage.test.ts +++ b/plugins/codex-usage-source/server/usage.test.ts @@ -103,7 +103,31 @@ test("coerces credit balance and marks a 96 percent window dangerous", async () expect.objectContaining({ id: "weekly", tone: "danger" }), ]), ); - expect(report.balances).toEqual([expect.objectContaining({ remaining: 0, tone: "danger" })]); + expect(report.balances).toEqual([ + expect.objectContaining({ remaining: 0, unit: "credits", tone: "danger" }), + ]); +}); + +test("summarizes the session and weekly windows by default, not code review", async () => { + const report = await fetchUsage( + {}, + async () => + new Response( + JSON.stringify({ + rate_limit: { + primary_window: { used_percent: 12 }, + secondary_window: { used_percent: 40 }, + }, + code_review_rate_limit: { primary_window: { used_percent: 5 } }, + }), + { status: 200 }, + ), + ); + expect(report.windows.map((window) => [window.id, window.summary ?? false])).toEqual([ + ["session", true], + ["weekly", true], + ["code_review", false], + ]); }); test("HTML usage body is unavailable", async () => { diff --git a/plugins/codex-usage-source/server/usage.ts b/plugins/codex-usage-source/server/usage.ts index 3fb660ae09..8620484b6e 100644 --- a/plugins/codex-usage-source/server/usage.ts +++ b/plugins/codex-usage-source/server/usage.ts @@ -57,15 +57,13 @@ export async function readAuth( } function usageWindow( - id: string, - label: string, + spec: { id: string; label: string; shortLabel: string; summary?: boolean }, value: z.infer | null | undefined, ): UsageWindow | null { if (!value) return null; const usedPct = value.used_percent ?? 0; return windowFromUsedPct({ - id, - label, + ...spec, utilizationPct: usedPct, resetsAt: value.reset_at != null ? new Date(value.reset_at * 1000).toISOString() : null, tone: toneFromUsedPct(usedPct), @@ -95,9 +93,18 @@ export async function fetchUsage( if (text.trim().startsWith("<")) return { status: "unavailable", windows: [] }; const usage = responseSchema.parse(JSON.parse(text)); const windows = [ - usageWindow("session", "Session", usage.rate_limit?.primary_window), - usageWindow("weekly", "Weekly", usage.rate_limit?.secondary_window), - usageWindow("code_review", "Code review", usage.code_review_rate_limit?.primary_window), + usageWindow( + { id: "session", label: "Session", shortLabel: "5h", summary: true }, + usage.rate_limit?.primary_window, + ), + usageWindow( + { id: "weekly", label: "Weekly", shortLabel: "wk", summary: true }, + usage.rate_limit?.secondary_window, + ), + usageWindow( + { id: "code_review", label: "Code review", shortLabel: "review" }, + usage.code_review_rate_limit?.primary_window, + ), ].filter((window): window is UsageWindow => window !== null); const balance = usage.credits?.balance; return { @@ -112,7 +119,7 @@ export async function fetchUsage( id: "credits", label: "Credits", remaining: balance, - unit: "usd", + unit: "credits", tone: balanceToneFromRemaining(balance), }, ], diff --git a/plugins/grok-usage-source/server/usage.ts b/plugins/grok-usage-source/server/usage.ts index ba4ab829cf..65367070b4 100644 --- a/plugins/grok-usage-source/server/usage.ts +++ b/plugins/grok-usage-source/server/usage.ts @@ -98,6 +98,7 @@ function grokUsageWindow(response: z.infer): Usa return windowFromUsedPct({ id: weekly ? "weekly" : "monthly", label: weekly ? "Weekly" : "Monthly", + shortLabel: weekly ? "wk" : "mo", utilizationPct: percent, resetsAt: period?.end ?? null, tone: toneFromUsedPct(percent), diff --git a/plugins/muse-provider/server/usage.ts b/plugins/muse-provider/server/usage.ts index a60b0a34b2..36dae811d8 100644 --- a/plugins/muse-provider/server/usage.ts +++ b/plugins/muse-provider/server/usage.ts @@ -78,6 +78,8 @@ export class Usage { windowFromUsedPct({ id: "five_hour", label: `${usage.window.windowDurationMins / 60} hours`, + shortLabel: `${usage.window.windowDurationMins / 60}h`, + summary: true, utilizationPct: usage.window.usedPercent, resetsAt: new Date(usage.window.resetsAtMs).toISOString(), tone: toneFromUsedPct(usage.window.usedPercent), @@ -85,6 +87,8 @@ export class Usage { windowFromUsedPct({ id: "weekly", label: "Weekly", + shortLabel: "wk", + summary: true, utilizationPct: usage.weekly.usedPercent, resetsAt: new Date(usage.weekly.resetsAtMs).toISOString(), tone: toneFromUsedPct(usage.weekly.usedPercent), diff --git a/plugins/opencode-go-usage-source/server/usage.test.ts b/plugins/opencode-go-usage-source/server/usage.test.ts index c475d96b36..849fbe874f 100644 --- a/plugins/opencode-go-usage-source/server/usage.test.ts +++ b/plugins/opencode-go-usage-source/server/usage.test.ts @@ -59,6 +59,8 @@ test("discovers and fetches the default key from read-only auth.json", async () ["weekly", 42, "2026-09-28T00:00:00.000Z"], ["monthly", 100, "2026-10-01T00:00:00.000Z"], ]); + // The rolling window's length is not reported, so its percent stands without a name. + expect(report.windows.map((window) => window.shortLabel)).toEqual(["", "wk", "mo"]); // The current upstream endpoint returns windows only; do not invent balances. expect(report.balances).toBeUndefined(); expect(await readFile(path, "utf8")).toBe(content); diff --git a/plugins/opencode-go-usage-source/server/usage.ts b/plugins/opencode-go-usage-source/server/usage.ts index dd9944544e..77e83613df 100644 --- a/plugins/opencode-go-usage-source/server/usage.ts +++ b/plugins/opencode-go-usage-source/server/usage.ts @@ -65,14 +65,16 @@ export async function fetchUsage( const data = responseSchema.parse(await response.json()); const windows = ( [ - ["rolling", "Rolling", data.usage.rolling], - ["weekly", "Weekly", data.usage.weekly], - ["monthly", "Monthly", data.usage.monthly], + // The rolling window's length is not reported, so its percent stands without a name. + ["rolling", "Rolling", "", data.usage.rolling], + ["weekly", "Weekly", "wk", data.usage.weekly], + ["monthly", "Monthly", "mo", data.usage.monthly], ] as const - ).map(([id, label, value]) => + ).map(([id, label, shortLabel, value]) => windowFromUsedPct({ id, label, + shortLabel, utilizationPct: value.percent, resetsAt: value.resetsAt, tone: toneFromUsedPct(value.percent), From d30e99c858b93e0d469e22d1da7802318f22d5e3 Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Thu, 1 Oct 2026 06:56:46 +0200 Subject: [PATCH 041/100] Log why a resumed Claude agent has no history (#5818) When a resumed Claude agent's transcript was missing, history loading returned without a log line, and when reading or parsing the transcript threw, the error was swallowed. Either way the agent opened with an empty timeline and daemon.log said nothing. A missing transcript is now logged at info with the session id, cwd and the path that was checked, and a read or parse failure is logged as a warning with the error. Closes #5438 Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> Co-authored-by: Gabriel Giordano --- .../claude/agent.history-load.test.ts | 93 +++++++++++++++++++ .../server/agent/providers/claude/agent.ts | 14 ++- 2 files changed, 104 insertions(+), 3 deletions(-) create mode 100644 packages/server/src/server/agent/providers/claude/agent.history-load.test.ts diff --git a/packages/server/src/server/agent/providers/claude/agent.history-load.test.ts b/packages/server/src/server/agent/providers/claude/agent.history-load.test.ts new file mode 100644 index 0000000000..223e2001f5 --- /dev/null +++ b/packages/server/src/server/agent/providers/claude/agent.history-load.test.ts @@ -0,0 +1,93 @@ +import { mkdirSync, mkdtempSync, rmSync } from "node:fs"; +import * as os from "node:os"; +import * as path from "node:path"; +import pino from "pino"; +import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; + +import { ClaudeAgentClient } from "./agent.js"; +import { claudeProjectDirSync } from "./project-dir.js"; + +/** + * A resumed Claude agent whose transcript is missing or unreadable opens with an empty timeline. + * The daemon log at its default level has to say which, and where it looked. + */ + +const SESSION_ID = "history-load-session"; + +interface LogRecord { + level: number; + msg: string; + sessionId?: string; + historyPath?: string; + err?: { message?: string }; +} + +describe("ClaudeAgentSession persisted history load", () => { + let tempRoot: string; + let cwd: string; + let configDir: string; + let records: LogRecord[]; + + async function resume(): Promise { + const client = new ClaudeAgentClient({ + logger: pino({ level: "info" }, { write: (line: string) => records.push(JSON.parse(line)) }), + queryFactory: vi.fn(() => { + throw new Error("history load must not start a query"); + }), + resolveVersion: async () => "2.1.220", + }); + const session = await client.resumeSession( + { provider: "claude", sessionId: SESSION_ID }, + { cwd }, + ); + await session.close(); + } + + function transcriptPath(): string { + return path.join(claudeProjectDirSync(cwd, { configDir }), `${SESSION_ID}.jsonl`); + } + + beforeEach(() => { + tempRoot = mkdtempSync(path.join(os.tmpdir(), "claude-history-load-")); + cwd = path.join(tempRoot, "repo"); + configDir = path.join(tempRoot, "claude-config"); + mkdirSync(cwd, { recursive: true }); + vi.stubEnv("CLAUDE_CONFIG_DIR", configDir); + records = []; + }); + + afterEach(() => { + vi.unstubAllEnvs(); + rmSync(tempRoot, { recursive: true, force: true }); + }); + + test("logs the resolved transcript path when the transcript does not exist", async () => { + await resume(); + + expect(records).toContainEqual( + expect.objectContaining({ + level: pino.levels.values.info, + msg: "No Claude transcript to load history from", + sessionId: SESSION_ID, + historyPath: transcriptPath(), + }), + ); + }); + + test("logs a warning with the error when the transcript cannot be read", async () => { + // A directory at the transcript path exists but cannot be read as a file. + mkdirSync(transcriptPath(), { recursive: true }); + + await resume(); + + expect(records).toContainEqual( + expect.objectContaining({ + level: pino.levels.values.warn, + msg: "Failed to load Claude history from transcript", + sessionId: SESSION_ID, + historyPath: transcriptPath(), + err: expect.objectContaining({ message: expect.stringContaining("EISDIR") }), + }), + ); + }); +}); diff --git a/packages/server/src/server/agent/providers/claude/agent.ts b/packages/server/src/server/agent/providers/claude/agent.ts index b6a366370b..e5153e2102 100644 --- a/packages/server/src/server/agent/providers/claude/agent.ts +++ b/packages/server/src/server/agent/providers/claude/agent.ts @@ -4881,10 +4881,15 @@ class ClaudeAgentSession implements AgentSession { } private loadPersistedHistory(sessionId: string): void { + let historyPath: string | null = null; try { this.taskState.reset(); - const historyPath = this.resolveHistoryPath(sessionId); + historyPath = this.resolveHistoryPath(sessionId); if (!historyPath || !fs.existsSync(historyPath)) { + this.logger.info( + { sessionId, cwd: this.config.cwd, historyPath }, + "No Claude transcript to load history from", + ); return; } const content = fs.readFileSync(historyPath, "utf8"); @@ -4893,8 +4898,11 @@ class ClaudeAgentSession implements AgentSession { readClaudeSidechainHistory(historyPath), ); this.ingestPersistedHistory(content, replay); - } catch { - // ignore history load failures + } catch (error) { + this.logger.warn( + { err: error, sessionId, historyPath }, + "Failed to load Claude history from transcript", + ); } } From 06213e49bf42442d208df405ed4c215e1e45f690 Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Thu, 1 Oct 2026 10:11:05 +0200 Subject: [PATCH 042/100] Complete every Tintinweb subagent in a grouped completion (#5826) Tintinweb batches completions that land close together into one subagent-notification, with the first agent at the top level and the rest in details.others. The adapter only read the top level, so every other agent in the group stayed running in the Subagents track, live and after a reload. Closes #5822 Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> --- .../fixtures/background-group.json | 419 ++++++++++++++++++ .../tintinweb-pi-subagents/index.test.ts | 14 + .../tintinweb-pi-subagents/index.ts | 42 +- 3 files changed, 458 insertions(+), 17 deletions(-) create mode 100644 packages/server/src/server/agent/providers/pi/extensions/tintinweb-pi-subagents/fixtures/background-group.json diff --git a/packages/server/src/server/agent/providers/pi/extensions/tintinweb-pi-subagents/fixtures/background-group.json b/packages/server/src/server/agent/providers/pi/extensions/tintinweb-pi-subagents/fixtures/background-group.json new file mode 100644 index 0000000000..083e57744e --- /dev/null +++ b/packages/server/src/server/agent/providers/pi/extensions/tintinweb-pi-subagents/fixtures/background-group.json @@ -0,0 +1,419 @@ +{ + "provenance": { + "package": "@tintinweb/pi-subagents", + "version": "0.19.0", + "sourceCommit": "4f572eaa04c09d3dbc16e4a5f13a16b295e84e14", + "piVersion": "0.84.4", + "captureDate": "2026-10-01" + }, + "events": [ + { + "type": "tool_execution_start", + "toolCallId": "call_277173", + "toolName": "Agent", + "args": { + "run_in_background": true, + "description": "Test agent 1", + "prompt": "reply with exactly OK", + "subagent_type": "general-purpose" + } + }, + { + "type": "tool_execution_start", + "toolCallId": "call_277176", + "toolName": "Agent", + "args": { + "prompt": "reply with exactly OK", + "run_in_background": true, + "description": "Test agent 2", + "subagent_type": "general-purpose" + } + }, + { + "type": "tool_execution_start", + "toolCallId": "call_277179", + "toolName": "Agent", + "args": { + "subagent_type": "general-purpose", + "description": "Test agent 3", + "prompt": "reply with exactly OK", + "run_in_background": true + } + }, + { + "type": "tool_execution_end", + "toolCallId": "call_277173", + "toolName": "Agent", + "result": { + "content": [ + { + "type": "text", + "text": "Agent started in background.\nAgent ID: 4b4fd01d-aaa8-46d\nType: Agent\nDescription: Test agent 1\nOutput file: /tmp/pi-subagents-1000/tmp-paseo-pi-subagent-fixture/01a0f602-2dd3-7755-8969-96e4d126cd2e/tasks/4b4fd01d-aaa8-46d.output\n\nYou will be notified when this agent completes.\nUse get_subagent_result to retrieve full results, or steer_subagent to send it messages.\nDo not duplicate this agent's work." + } + ], + "details": { + "displayName": "Agent", + "description": "Test agent 1", + "subagentType": "general-purpose", + "modelName": "google: gemini 3.6 flash", + "tags": ["twin", "background"], + "toolUses": 0, + "tokens": "", + "durationMs": 0, + "status": "background", + "agentId": "4b4fd01d-aaa8-46d" + } + }, + "isError": false + }, + { + "type": "tool_execution_end", + "toolCallId": "call_277176", + "toolName": "Agent", + "result": { + "content": [ + { + "type": "text", + "text": "Agent started in background.\nAgent ID: a92b022c-cd9e-41e\nType: Agent\nDescription: Test agent 2\nOutput file: /tmp/pi-subagents-1000/tmp-paseo-pi-subagent-fixture/01a0f602-2dd3-7755-8969-96e4d126cd2e/tasks/a92b022c-cd9e-41e.output\n\nYou will be notified when this agent completes.\nUse get_subagent_result to retrieve full results, or steer_subagent to send it messages.\nDo not duplicate this agent's work." + } + ], + "details": { + "displayName": "Agent", + "description": "Test agent 2", + "subagentType": "general-purpose", + "modelName": "google: gemini 3.6 flash", + "tags": ["twin", "background"], + "toolUses": 0, + "tokens": "", + "durationMs": 0, + "status": "background", + "agentId": "a92b022c-cd9e-41e" + } + }, + "isError": false + }, + { + "type": "tool_execution_end", + "toolCallId": "call_277179", + "toolName": "Agent", + "result": { + "content": [ + { + "type": "text", + "text": "Agent started in background.\nAgent ID: 13336b9f-79c3-49e\nType: Agent\nDescription: Test agent 3\nOutput file: /tmp/pi-subagents-1000/tmp-paseo-pi-subagent-fixture/01a0f602-2dd3-7755-8969-96e4d126cd2e/tasks/13336b9f-79c3-49e.output\n\nYou will be notified when this agent completes.\nUse get_subagent_result to retrieve full results, or steer_subagent to send it messages.\nDo not duplicate this agent's work." + } + ], + "details": { + "displayName": "Agent", + "description": "Test agent 3", + "subagentType": "general-purpose", + "modelName": "google: gemini 3.6 flash", + "tags": ["twin", "background"], + "toolUses": 0, + "tokens": "", + "durationMs": 0, + "status": "background", + "agentId": "13336b9f-79c3-49e" + } + }, + "isError": false + }, + { + "type": "message_end", + "message": { + "role": "custom", + "customType": "subagent-notification", + "content": "Background agent group completed: 3 agent(s) finished\n\n\n4b4fd01d-aaa8-46d\ncall_277173\n/tmp/pi-subagents-1000/tmp-paseo-pi-subagent-fixture/01a0f602-2dd3-7755-8969-96e4d126cd2e/tasks/4b4fd01d-aaa8-46d.output\nDone\nAgent \"Test agent 1\" completed\nOK\n10186011054\n\n\n\na92b022c-cd9e-41e\ncall_277176\n/tmp/pi-subagents-1000/tmp-paseo-pi-subagent-fixture/01a0f602-2dd3-7755-8969-96e4d126cd2e/tasks/a92b022c-cd9e-41e.output\nDone\nAgent \"Test agent 2\" completed\nOK\n10186011182\n\n\n\n13336b9f-79c3-49e\ncall_277179\n/tmp/pi-subagents-1000/tmp-paseo-pi-subagent-fixture/01a0f602-2dd3-7755-8969-96e4d126cd2e/tasks/13336b9f-79c3-49e.output\nDone\nAgent \"Test agent 3\" completed\nOK\n10186011472\n\n\nUse get_subagent_result for full output.", + "display": true, + "details": { + "id": "4b4fd01d-aaa8-46d", + "description": "Test agent 1", + "status": "completed", + "toolUses": 0, + "turnCount": 0, + "totalTokens": 10186, + "totalCost": 0.007642500000000001, + "durationMs": 1054, + "outputFile": "/tmp/pi-subagents-1000/tmp-paseo-pi-subagent-fixture/01a0f602-2dd3-7755-8969-96e4d126cd2e/tasks/4b4fd01d-aaa8-46d.output", + "resultPreview": "OK", + "others": [ + { + "id": "a92b022c-cd9e-41e", + "description": "Test agent 2", + "status": "completed", + "toolUses": 0, + "turnCount": 0, + "totalTokens": 10186, + "totalCost": 0.007642500000000001, + "durationMs": 1182, + "outputFile": "/tmp/pi-subagents-1000/tmp-paseo-pi-subagent-fixture/01a0f602-2dd3-7755-8969-96e4d126cd2e/tasks/a92b022c-cd9e-41e.output", + "resultPreview": "OK" + }, + { + "id": "13336b9f-79c3-49e", + "description": "Test agent 3", + "status": "completed", + "toolUses": 0, + "turnCount": 0, + "totalTokens": 10186, + "totalCost": 0.007642500000000001, + "durationMs": 1472, + "outputFile": "/tmp/pi-subagents-1000/tmp-paseo-pi-subagent-fixture/01a0f602-2dd3-7755-8969-96e4d126cd2e/tasks/13336b9f-79c3-49e.output", + "resultPreview": "OK" + } + ] + }, + "timestamp": 1790833737082 + } + } + ], + "messages": [ + { + "role": "assistant", + "content": [ + { + "type": "thinking", + "thinking": "", + "thinkingSignature": "[{\"type\":\"reasoning.encrypted\",\"data\":\"AY89a19lxhI31c8Kjto2eEqQuEoAa1jXk+6WHw4RxcJiLq9eTkm6UtOU0r9mP+xFrs3A9GkNyIfpaIzLUGuPKjiYw648QdaOOVbR7jCqPVFA+CEYQH0V\",\"format\":\"google-gemini-v1\",\"id\":\"call_277173\",\"index\":0}]" + }, + { + "type": "toolCall", + "id": "call_277173", + "name": "Agent", + "arguments": { + "run_in_background": true, + "description": "Test agent 1", + "prompt": "reply with exactly OK", + "subagent_type": "general-purpose" + } + }, + { + "type": "toolCall", + "id": "call_277176", + "name": "Agent", + "arguments": { + "prompt": "reply with exactly OK", + "run_in_background": true, + "description": "Test agent 2", + "subagent_type": "general-purpose" + } + }, + { + "type": "toolCall", + "id": "call_277179", + "name": "Agent", + "arguments": { + "subagent_type": "general-purpose", + "description": "Test agent 3", + "prompt": "reply with exactly OK", + "run_in_background": true + } + } + ], + "api": "openai-completions", + "provider": "openrouter", + "model": "google/gemini-3.6-flash", + "usage": { + "input": 13147, + "output": 135, + "cacheRead": 0, + "cacheWrite": 0, + "reasoning": 0, + "totalTokens": 13282, + "cost": { + "input": 0.009860250000000001, + "output": 0.00050625, + "cacheRead": 0, + "cacheWrite": 0, + "total": 0.0103665 + } + }, + "stopReason": "toolUse", + "timestamp": 1790833733536, + "responseId": "gen-1790833733-Qo0mjOAU6129xnYfl8rM", + "rawStopReason": "tool_calls" + }, + { + "role": "toolResult", + "toolCallId": "call_277173", + "toolName": "Agent", + "content": [ + { + "type": "text", + "text": "Agent started in background.\nAgent ID: 4b4fd01d-aaa8-46d\nType: Agent\nDescription: Test agent 1\nOutput file: /tmp/pi-subagents-1000/tmp-paseo-pi-subagent-fixture/01a0f602-2dd3-7755-8969-96e4d126cd2e/tasks/4b4fd01d-aaa8-46d.output\n\nYou will be notified when this agent completes.\nUse get_subagent_result to retrieve full results, or steer_subagent to send it messages.\nDo not duplicate this agent's work." + } + ], + "details": { + "displayName": "Agent", + "description": "Test agent 1", + "subagentType": "general-purpose", + "modelName": "google: gemini 3.6 flash", + "tags": ["twin", "background"], + "toolUses": 0, + "tokens": "", + "durationMs": 0, + "status": "background", + "agentId": "4b4fd01d-aaa8-46d" + }, + "isError": false, + "timestamp": 1790833735411 + }, + { + "role": "toolResult", + "toolCallId": "call_277176", + "toolName": "Agent", + "content": [ + { + "type": "text", + "text": "Agent started in background.\nAgent ID: a92b022c-cd9e-41e\nType: Agent\nDescription: Test agent 2\nOutput file: /tmp/pi-subagents-1000/tmp-paseo-pi-subagent-fixture/01a0f602-2dd3-7755-8969-96e4d126cd2e/tasks/a92b022c-cd9e-41e.output\n\nYou will be notified when this agent completes.\nUse get_subagent_result to retrieve full results, or steer_subagent to send it messages.\nDo not duplicate this agent's work." + } + ], + "details": { + "displayName": "Agent", + "description": "Test agent 2", + "subagentType": "general-purpose", + "modelName": "google: gemini 3.6 flash", + "tags": ["twin", "background"], + "toolUses": 0, + "tokens": "", + "durationMs": 0, + "status": "background", + "agentId": "a92b022c-cd9e-41e" + }, + "isError": false, + "timestamp": 1790833735412 + }, + { + "role": "toolResult", + "toolCallId": "call_277179", + "toolName": "Agent", + "content": [ + { + "type": "text", + "text": "Agent started in background.\nAgent ID: 13336b9f-79c3-49e\nType: Agent\nDescription: Test agent 3\nOutput file: /tmp/pi-subagents-1000/tmp-paseo-pi-subagent-fixture/01a0f602-2dd3-7755-8969-96e4d126cd2e/tasks/13336b9f-79c3-49e.output\n\nYou will be notified when this agent completes.\nUse get_subagent_result to retrieve full results, or steer_subagent to send it messages.\nDo not duplicate this agent's work." + } + ], + "details": { + "displayName": "Agent", + "description": "Test agent 3", + "subagentType": "general-purpose", + "modelName": "google: gemini 3.6 flash", + "tags": ["twin", "background"], + "toolUses": 0, + "tokens": "", + "durationMs": 0, + "status": "background", + "agentId": "13336b9f-79c3-49e" + }, + "isError": false, + "timestamp": 1790833735412 + }, + { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "I have spawned the 3 background agents and am waiting for their completion notifications." + } + ], + "api": "openai-completions", + "provider": "openrouter", + "model": "google/gemini-3.6-flash", + "usage": { + "input": 2004, + "output": 16, + "cacheRead": 11743, + "cacheWrite": 0, + "reasoning": 0, + "totalTokens": 13763, + "cost": { + "input": 0.001503, + "output": 6e-5, + "cacheRead": 0.000880725, + "cacheWrite": 0, + "total": 0.002443725 + } + }, + "stopReason": "stop", + "timestamp": 1790833735412, + "responseId": "gen-1790833735-adM3whTd4mLRXnKi2Xu0", + "rawStopReason": "stop" + }, + { + "role": "custom", + "customType": "subagent-notification", + "content": "Background agent group completed: 3 agent(s) finished\n\n\n4b4fd01d-aaa8-46d\ncall_277173\n/tmp/pi-subagents-1000/tmp-paseo-pi-subagent-fixture/01a0f602-2dd3-7755-8969-96e4d126cd2e/tasks/4b4fd01d-aaa8-46d.output\nDone\nAgent \"Test agent 1\" completed\nOK\n10186011054\n\n\n\na92b022c-cd9e-41e\ncall_277176\n/tmp/pi-subagents-1000/tmp-paseo-pi-subagent-fixture/01a0f602-2dd3-7755-8969-96e4d126cd2e/tasks/a92b022c-cd9e-41e.output\nDone\nAgent \"Test agent 2\" completed\nOK\n10186011182\n\n\n\n13336b9f-79c3-49e\ncall_277179\n/tmp/pi-subagents-1000/tmp-paseo-pi-subagent-fixture/01a0f602-2dd3-7755-8969-96e4d126cd2e/tasks/13336b9f-79c3-49e.output\nDone\nAgent \"Test agent 3\" completed\nOK\n10186011472\n\n\nUse get_subagent_result for full output.", + "display": true, + "details": { + "id": "4b4fd01d-aaa8-46d", + "description": "Test agent 1", + "status": "completed", + "toolUses": 0, + "turnCount": 0, + "totalTokens": 10186, + "totalCost": 0.007642500000000001, + "durationMs": 1054, + "outputFile": "/tmp/pi-subagents-1000/tmp-paseo-pi-subagent-fixture/01a0f602-2dd3-7755-8969-96e4d126cd2e/tasks/4b4fd01d-aaa8-46d.output", + "resultPreview": "OK", + "others": [ + { + "id": "a92b022c-cd9e-41e", + "description": "Test agent 2", + "status": "completed", + "toolUses": 0, + "turnCount": 0, + "totalTokens": 10186, + "totalCost": 0.007642500000000001, + "durationMs": 1182, + "outputFile": "/tmp/pi-subagents-1000/tmp-paseo-pi-subagent-fixture/01a0f602-2dd3-7755-8969-96e4d126cd2e/tasks/a92b022c-cd9e-41e.output", + "resultPreview": "OK" + }, + { + "id": "13336b9f-79c3-49e", + "description": "Test agent 3", + "status": "completed", + "toolUses": 0, + "turnCount": 0, + "totalTokens": 10186, + "totalCost": 0.007642500000000001, + "durationMs": 1472, + "outputFile": "/tmp/pi-subagents-1000/tmp-paseo-pi-subagent-fixture/01a0f602-2dd3-7755-8969-96e4d126cd2e/tasks/13336b9f-79c3-49e.output", + "resultPreview": "OK" + } + ] + }, + "timestamp": 1790833737082 + }, + { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "DONE" + } + ], + "api": "openai-completions", + "provider": "openrouter", + "model": "google/gemini-3.6-flash", + "usage": { + "input": 2740, + "output": 1, + "cacheRead": 11762, + "cacheWrite": 0, + "reasoning": 0, + "totalTokens": 14503, + "cost": { + "input": 0.002055, + "output": 3.75e-6, + "cacheRead": 0.00088215, + "cacheWrite": 0, + "total": 0.0029408999999999998 + } + }, + "stopReason": "stop", + "timestamp": 1790833737083, + "responseId": "gen-1790833737-3IfsB2wLuE8DMNlJMgmI", + "rawStopReason": "stop" + } + ] +} diff --git a/packages/server/src/server/agent/providers/pi/extensions/tintinweb-pi-subagents/index.test.ts b/packages/server/src/server/agent/providers/pi/extensions/tintinweb-pi-subagents/index.test.ts index b464026ab1..b03c17c6ec 100644 --- a/packages/server/src/server/agent/providers/pi/extensions/tintinweb-pi-subagents/index.test.ts +++ b/packages/server/src/server/agent/providers/pi/extensions/tintinweb-pi-subagents/index.test.ts @@ -51,6 +51,20 @@ describe("@tintinweb/pi-subagents adapter", () => { ).toEqual(["running", "running", "completed"]); expect(events.filter((event) => event.event.type === "timeline").length).toBeGreaterThan(0); }); + test("completes every background child in a grouped notification", async () => { + const events = await verifySubagentFixture( + readSubagentFixture(new URL("./fixtures/background-group.json", import.meta.url)), + ); + const finalStatus = new Map(); + for (const { event } of events) { + if (event.type === "upsert" && event.status) finalStatus.set(event.id, event.status); + } + expect(Object.fromEntries(finalStatus)).toEqual({ + call_277173: "completed", + call_277176: "completed", + call_277179: "completed", + }); + }); test("declines foreign Agent results", () => { expect( createPiExtensionHost().mapToolCall({ diff --git a/packages/server/src/server/agent/providers/pi/extensions/tintinweb-pi-subagents/index.ts b/packages/server/src/server/agent/providers/pi/extensions/tintinweb-pi-subagents/index.ts index 2b06779afa..9c6ef9917e 100644 --- a/packages/server/src/server/agent/providers/pi/extensions/tintinweb-pi-subagents/index.ts +++ b/packages/server/src/server/agent/providers/pi/extensions/tintinweb-pi-subagents/index.ts @@ -19,7 +19,7 @@ const Details = z description: z.string().optional(), }) .passthrough(); -const Notification = z +const NotificationEntry = z .object({ id: z.string().trim().min(1), status: z.string(), @@ -27,6 +27,11 @@ const Notification = z outputFile: z.string().trim().min(1).optional(), }) .passthrough(); +// Completions that land close together arrive as one notification: the first +// agent at the top level and the rest in `others`. +const Notification = NotificationEntry.extend({ + others: z.array(NotificationEntry).optional(), +}); const status = (value: string): "running" | "completed" | "failed" | "canceled" => { if (value === "completed") return "completed"; if (value === "error") return "failed"; @@ -127,22 +132,25 @@ export const tintinwebPiSubagents: PiExtension = { return undefined; const details = Notification.safeParse(message.details); if (!details.success) return undefined; - const id = callsByAgent.get(details.data.id); - if (!id) return undefined; - const file = details.data.outputFile; - const childSessions = file && !readSessions.has(file) ? [{ id, file }] : []; - if (childSessions.length && file) readSessions.add(file); - return { - subagents: [ - { - type: "upsert", - id, - description: details.data.description, - status: status(details.data.status), - }, - ], - childSessions, - }; + const subagents = []; + const childSessions = []; + for (const entry of [details.data, ...(details.data.others ?? [])]) { + const id = callsByAgent.get(entry.id); + if (!id) continue; + const file = entry.outputFile; + if (file && !readSessions.has(file)) { + readSessions.add(file); + childSessions.push({ id, file }); + } + subagents.push({ + type: "upsert" as const, + id, + description: entry.description, + status: status(entry.status), + }); + } + if (subagents.length === 0) return undefined; + return { subagents, childSessions }; }, }; }, From bdb8e132219d3fb72d35ef190c54c10400a0a80f Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Thu, 1 Oct 2026 10:40:30 +0200 Subject: [PATCH 043/100] docs(changelog): add 0.11.0-beta.1 release notes --- CHANGELOG.md | 72 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 68bf52ca78..77f2ef72e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,77 @@ # Changelog +## 0.11.0-beta.1 - 2026-10-01 + +### Added + +- Added Muse Code as a provider, with model, approval mode, and reasoning effort controls ([#5719](https://github.com/getpaseo/paseo/pull/5719), [#5775](https://github.com/getpaseo/paseo/pull/5775) by [@millerben95](https://github.com/millerben95)) +- Added Antigravity as a provider, driving the installed `agy` CLI in Full access ([#5714](https://github.com/getpaseo/paseo/pull/5714), [#5795](https://github.com/getpaseo/paseo/pull/5795)) +- Added Usage to the sidebar footer, showing each account's percent or the windows you pin ([#5465](https://github.com/getpaseo/paseo/pull/5465), [#5685](https://github.com/getpaseo/paseo/pull/5685), [#5786](https://github.com/getpaseo/paseo/pull/5786), [#5805](https://github.com/getpaseo/paseo/pull/5805)) +- Added a Usage screen that follows the account each session spends, including custom `CLAUDE_CONFIG_DIR` and `CODEX_HOME` homes and OpenCode Go ([#5465](https://github.com/getpaseo/paseo/pull/5465)) +- Added mid-turn steering to OMP agents ([#5550](https://github.com/getpaseo/paseo/pull/5550) by [@n24q02m](https://github.com/n24q02m)) +- Added Fast mode and an Auto thinking level to OMP ([#5550](https://github.com/getpaseo/paseo/pull/5550) by [@n24q02m](https://github.com/n24q02m), [@abhi-wan-kenobi](https://github.com/abhi-wan-kenobi)) +- Added MCP servers to OMP schedules and Hub runs ([#5550](https://github.com/getpaseo/paseo/pull/5550) by [@omercnet](https://github.com/omercnet)) +- Added Codex Fast and Ultrafast speeds from the Codex model catalog, with a lightning-bolt trigger on desktop ([#5708](https://github.com/getpaseo/paseo/pull/5708)) +- Added a Content width setting for the chat and Markdown preview ([#5680](https://github.com/getpaseo/paseo/pull/5680)) +- Added a confirmation before a pairing link connects to a new host or to a saved host whose key or relay changed ([#5753](https://github.com/getpaseo/paseo/pull/5753)) +- Added provider option defaults in `config.json` under `agents.providers..options`, with per-agent overrides, for every provider ([#5780](https://github.com/getpaseo/paseo/pull/5780)) +- Added command and environment overrides and availability diagnostics for plugin providers ([#5707](https://github.com/getpaseo/paseo/pull/5707)) +- Added sidebar header and footer items and screens to the plugin SDK ([#5685](https://github.com/getpaseo/paseo/pull/5685)) +- Added `activeTurnBehavior` to the SDK's `send()` and `run()` so a caller can steer instead of interrupt an active turn ([#5605](https://github.com/getpaseo/paseo/pull/5605) by [@wjxdy](https://github.com/wjxdy)) +- Added the terminal key bar and Paste to terminal panes on iPad and Android tablets ([#5712](https://github.com/getpaseo/paseo/pull/5712) by [@lardissone](https://github.com/lardissone)) + +### Changed + +- Moved Import session into New workspace ([#5805](https://github.com/getpaseo/paseo/pull/5805)) +- Changed new workspaces branched from `origin/main` or another remote branch to fetch that branch first ([#5788](https://github.com/getpaseo/paseo/pull/5788) by [@odoo-mvds](https://github.com/odoo-mvds)) +- Changed Paseo's internal Claude agents for branch names, commit messages, and PR text to run with Claude hooks disabled ([#5750](https://github.com/getpaseo/paseo/pull/5750)) +- Changed OMP approval mode changes to apply on the next idle relaunch, and to be refused during a turn ([#5550](https://github.com/getpaseo/paseo/pull/5550)) +- Changed relative timestamps in agent, schedule, and Import session rows to keep advancing while on screen ([#5340](https://github.com/getpaseo/paseo/pull/5340)) + +### Fixed + +- Fixed OMP agents staying bound to a dead `omp` process after it crashed ([#5550](https://github.com/getpaseo/paseo/pull/5550) by [@viet-initx](https://github.com/viet-initx)) +- Fixed OMP multi-select questions showing radio buttons and asking once per choice ([#5550](https://github.com/getpaseo/paseo/pull/5550)) +- Fixed `/compact` on OMP reporting a failure after 60s while OMP kept compacting ([#5550](https://github.com/getpaseo/paseo/pull/5550) by [@Kh05ifr4nD](https://github.com/Kh05ifr4nD)) +- Fixed `paseo import --provider omp` starting the agent on the default model instead of the session's ([#5550](https://github.com/getpaseo/paseo/pull/5550) by [@alloevil](https://github.com/alloevil)) +- Fixed OMP-injected rows showing a `[custom_message]` prefix after a reload ([#5550](https://github.com/getpaseo/paseo/pull/5550) by [@jegork](https://github.com/jegork)) +- Fixed OMP failed tools, Task, Wait, web search, fetch, and `xd://` rows showing raw objects or generic labels ([#5550](https://github.com/getpaseo/paseo/pull/5550)) +- Fixed results of OMP `xd://` writes disappearing from the timeline ([#5550](https://github.com/getpaseo/paseo/pull/5550) by [@kasrakhosravi](https://github.com/kasrakhosravi)) +- Fixed the composer showing the stale model after an OMP model fallback ([#5550](https://github.com/getpaseo/paseo/pull/5550) by [@gray-graff](https://github.com/gray-graff)) +- Fixed opening an archived OMP agent starting an `omp` process that stayed running ([#5550](https://github.com/getpaseo/paseo/pull/5550) by [@thomasvan](https://github.com/thomasvan)) +- Fixed Pi agents on Pi 0.99 starting without Paseo's MCP servers when `pi-mcp-adapter` is not installed ([#5762](https://github.com/getpaseo/paseo/pull/5762) by [@ArietidsZ](https://github.com/ArietidsZ)) +- Fixed a running Pi subagent showing an empty transcript until it finished ([#5755](https://github.com/getpaseo/paseo/pull/5755)) +- Fixed a grouped Tintinweb pi-subagents completion marking only its first agent completed and leaving the rest on working ([#5826](https://github.com/getpaseo/paseo/pull/5826)) +- Fixed OpenCode `ask` rules for `paseo_*` tools running the tool without a prompt ([#5801](https://github.com/getpaseo/paseo/pull/5801) by [@ymarcus93](https://github.com/ymarcus93)) +- Fixed a failed Codex image generation leaving no row in the chat ([#5810](https://github.com/getpaseo/paseo/pull/5810) by [@3ae3ae](https://github.com/3ae3ae)) +- Fixed a denied Codex command or file edit keeping a running spinner after the turn ended ([#5717](https://github.com/getpaseo/paseo/pull/5717) by [@3ae3ae](https://github.com/3ae3ae)) +- Fixed rewinding a legacy Codex chat failing with `unknown variant thread/rollback` on Codex 0.156 and newer ([#5711](https://github.com/getpaseo/paseo/pull/5711) by [@3ae3ae](https://github.com/3ae3ae)) +- Fixed Codex custom prompt arguments containing `$` sequences reaching Codex altered ([#5669](https://github.com/getpaseo/paseo/pull/5669) by [@3ae3ae](https://github.com/3ae3ae)) +- Fixed Codex structured output failing when the schema has a field named `properties` ([#5684](https://github.com/getpaseo/paseo/pull/5684)) +- Fixed a Codex agent created without a thinking option losing its default effort after a daemon restart ([#5686](https://github.com/getpaseo/paseo/pull/5686)) +- Fixed only one permission showing when Codex asks to approve two commands from one item ([#5758](https://github.com/getpaseo/paseo/pull/5758)) +- Fixed Codex terminal rows reading only "Terminal" without the command on current Codex ([#5774](https://github.com/getpaseo/paseo/pull/5774)) +- Fixed the composer showing the catalog default thinking level instead of the one the agent runs on ([#5804](https://github.com/getpaseo/paseo/pull/5804)) +- Fixed the composer showing another model's label when the agent's model is missing from the loaded catalog ([#5796](https://github.com/getpaseo/paseo/pull/5796)) +- Fixed schedule runs and workspace metadata recording `[object Object]` when an ACP agent rejects a session setting ([#5765](https://github.com/getpaseo/paseo/pull/5765)) +- Fixed opening an archived agent whose worktree was removed showing "Workspace unavailable" when the workspace had no recorded branch ([#5694](https://github.com/getpaseo/paseo/pull/5694)) +- Fixed a host's version staying stale on the host page after its daemon restarts ([#5351](https://github.com/getpaseo/paseo/pull/5351) by [@bashrusakh](https://github.com/bashrusakh)) +- Fixed removing a host during its first connection attempt not stopping the connection ([#5722](https://github.com/getpaseo/paseo/pull/5722)) +- Fixed a file upload started while reconnecting failing with `Connection changed during file upload` ([#5585](https://github.com/getpaseo/paseo/pull/5585)) +- Fixed reloading a plugin adding `[System Error] Provider connection closed` to finished chats on its provider ([#5579](https://github.com/getpaseo/paseo/pull/5579)) +- Fixed `paseo agent open` opening Desktop and exiting 0 for an agent that does not exist ([#5727](https://github.com/getpaseo/paseo/pull/5727)) +- Fixed the desktop app launching a `daemon status` process every second when no local daemon runs ([#5636](https://github.com/getpaseo/paseo/pull/5636)) +- Fixed browser annotation screenshots capturing the wrong region when the app is zoomed ([#5703](https://github.com/getpaseo/paseo/pull/5703)) +- Fixed the context ring filling from three o'clock instead of twelve on web and desktop ([#5598](https://github.com/getpaseo/paseo/pull/5598)) +- Fixed the `/` menu listing `/clear` and `/exit` in a draft before a project or model is chosen ([#5594](https://github.com/getpaseo/paseo/pull/5594) by [@colonelpanic8](https://github.com/colonelpanic8)) +- Fixed plugin settings screens on desktop having no way back to the Plugins page ([#5620](https://github.com/getpaseo/paseo/pull/5620)) +- Fixed the daemon logging `Agent not found` for every new agent started from a draft ([#5602](https://github.com/getpaseo/paseo/pull/5602)) +- Fixed `daemon.log` saying nothing when a resumed Claude agent's transcript is missing or cannot be read ([#5818](https://github.com/getpaseo/paseo/pull/5818) by [@gabrielgiordan](https://github.com/gabrielgiordan)) +- Fixed a dialog opened over another sheet on iPad failing to present ([#5772](https://github.com/getpaseo/paseo/pull/5772)) +- Fixed tapping the context ring and long-pressing a workspace tab doing nothing on native tablets ([#5713](https://github.com/getpaseo/paseo/pull/5713) by [@JichenZhang](https://github.com/JichenZhang)) +- Fixed iPad chat text keeping its narrow width after the Explorer closes ([#5716](https://github.com/getpaseo/paseo/pull/5716)) +- Fixed bundling `@getpaseo/client` from npm failing with `Could not resolve "@getpaseo/relay/e2ee"` ([#5815](https://github.com/getpaseo/paseo/pull/5815)) + ## 0.10.2 - 2026-09-30 ### Fixed From 4aa2f2817d9be5c6437c8063a9b840a0196228c5 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Thu, 1 Oct 2026 10:48:28 +0200 Subject: [PATCH 044/100] chore(release): cut 0.11.0-beta.1 --- package-lock.json | 56 ++++++++++++------------ package.json | 2 +- packages/app/package.json | 2 +- packages/cli/package.json | 8 ++-- packages/client/package.json | 6 +-- packages/desktop/package.json | 2 +- packages/expo-two-way-audio/package.json | 2 +- packages/highlight/package.json | 2 +- packages/plugin/package.json | 10 ++--- packages/protocol/package.json | 2 +- packages/relay/package.json | 2 +- packages/server/package.json | 12 ++--- packages/website/package.json | 2 +- plugins/package.json | 2 +- 14 files changed, 55 insertions(+), 55 deletions(-) diff --git a/package-lock.json b/package-lock.json index c396338f9b..e258c05730 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "paseo", - "version": "0.10.0", + "version": "0.11.0-beta.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "paseo", - "version": "0.10.0", + "version": "0.11.0-beta.1", "hasInstallScript": true, "license": "Apache-2.0", "workspaces": [ @@ -36415,7 +36415,7 @@ }, "packages/app": { "name": "@getpaseo/app", - "version": "0.10.0", + "version": "0.11.0-beta.1", "dependencies": { "@codemirror/commands": "6.10.4", "@codemirror/language": "6.12.4", @@ -37481,12 +37481,12 @@ }, "packages/cli": { "name": "@getpaseo/cli", - "version": "0.10.0", + "version": "0.11.0-beta.1", "dependencies": { "@clack/prompts": "^1.0.0", - "@getpaseo/client": "0.10.0", - "@getpaseo/protocol": "0.10.0", - "@getpaseo/server": "0.10.0", + "@getpaseo/client": "0.11.0-beta.1", + "@getpaseo/protocol": "0.11.0-beta.1", + "@getpaseo/server": "0.11.0-beta.1", "chalk": "^5.3.0", "commander": "^12.0.0", "mime-types": "^2.1.35", @@ -37733,10 +37733,10 @@ }, "packages/client": { "name": "@getpaseo/client", - "version": "0.10.0", + "version": "0.11.0-beta.1", "dependencies": { - "@getpaseo/protocol": "0.10.0", - "@getpaseo/relay": "0.10.0", + "@getpaseo/protocol": "0.11.0-beta.1", + "@getpaseo/relay": "0.11.0-beta.1", "zod": "^4.4.3" }, "devDependencies": { @@ -37747,7 +37747,7 @@ }, "packages/desktop": { "name": "@getpaseo/desktop", - "version": "0.10.0", + "version": "0.11.0-beta.1", "license": "Apache-2.0", "dependencies": { "@getpaseo/cli": "*", @@ -37991,7 +37991,7 @@ }, "packages/expo-two-way-audio": { "name": "@getpaseo/expo-two-way-audio", - "version": "0.10.0", + "version": "0.11.0-beta.1", "license": "MIT", "devDependencies": { "@types/jest": "^29.5.14", @@ -38887,7 +38887,7 @@ }, "packages/highlight": { "name": "@getpaseo/highlight", - "version": "0.10.0", + "version": "0.11.0-beta.1", "dependencies": { "@codemirror/language": "6.12.4", "@codemirror/legacy-modes": "^6.5.3", @@ -38916,14 +38916,14 @@ }, "packages/plugin": { "name": "@getpaseo/plugin", - "version": "0.10.0", + "version": "0.11.0-beta.1", "dependencies": { "@agentclientprotocol/sdk": "^1.4.0", "use-sync-external-store": "^1.6.0" }, "devDependencies": { - "@getpaseo/client": "0.10.0", - "@getpaseo/protocol": "0.10.0", + "@getpaseo/client": "0.11.0-beta.1", + "@getpaseo/protocol": "0.11.0-beta.1", "@types/node": "^20.9.0", "@types/react": "~19.2.0", "react": "19.1.0", @@ -38933,8 +38933,8 @@ "zod": "^4.4.3" }, "peerDependencies": { - "@getpaseo/client": "0.10.0", - "@getpaseo/protocol": "0.10.0", + "@getpaseo/client": "0.11.0-beta.1", + "@getpaseo/protocol": "0.11.0-beta.1", "react": "~19.1.0", "react-native": ">=0.81.5", "zod": "^4.4.3" @@ -38956,7 +38956,7 @@ }, "packages/protocol": { "name": "@getpaseo/protocol", - "version": "0.10.0", + "version": "0.11.0-beta.1", "dependencies": { "semver": "^7.8.5", "zod": "^4.4.3" @@ -38983,7 +38983,7 @@ }, "packages/relay": { "name": "@getpaseo/relay", - "version": "0.10.0", + "version": "0.11.0-beta.1", "dependencies": { "base64-js": "^1.5.1", "tweetnacl": "^1.0.3", @@ -39201,16 +39201,16 @@ }, "packages/server": { "name": "@getpaseo/server", - "version": "0.10.0", + "version": "0.11.0-beta.1", "dependencies": { "@agentclientprotocol/sdk": "^0.17.1", "@anthropic-ai/claude-agent-sdk": "0.3.246", "@anthropic-ai/sdk": "^0.104.2", - "@getpaseo/client": "0.10.0", - "@getpaseo/highlight": "0.10.0", - "@getpaseo/plugin": "0.10.0", - "@getpaseo/protocol": "0.10.0", - "@getpaseo/relay": "0.10.0", + "@getpaseo/client": "0.11.0-beta.1", + "@getpaseo/highlight": "0.11.0-beta.1", + "@getpaseo/plugin": "0.11.0-beta.1", + "@getpaseo/protocol": "0.11.0-beta.1", + "@getpaseo/relay": "0.11.0-beta.1", "@isaacs/ttlcache": "^2.1.4", "@modelcontextprotocol/sdk": "1.29.0", "@opencode-ai/sdk": "1.14.46", @@ -40131,7 +40131,7 @@ }, "packages/website": { "name": "@getpaseo/website", - "version": "0.10.0", + "version": "0.11.0-beta.1", "dependencies": { "@cloudflare/vite-plugin": "^1.29.1", "@cloudflare/workers-types": "^4.20260317.1", @@ -41736,7 +41736,7 @@ }, "plugins": { "name": "@getpaseo/builtin-plugins", - "version": "0.0.0", + "version": "0.11.0-beta.1", "dependencies": { "@getpaseo/plugin": "*", "zod": "^4.4.3" diff --git a/package.json b/package.json index 948cd29b27..fa5a536397 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "paseo", - "version": "0.10.0", + "version": "0.11.0-beta.1", "private": true, "description": "Paseo: voice-controlled development environment for local AI coding agents", "keywords": [ diff --git a/packages/app/package.json b/packages/app/package.json index 1a293d00a0..10dd5f0710 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/app", - "version": "0.10.0", + "version": "0.11.0-beta.1", "private": true, "main": "index.ts", "scripts": { diff --git a/packages/cli/package.json b/packages/cli/package.json index c113897cc6..4f9824a90f 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/cli", - "version": "0.10.0", + "version": "0.11.0-beta.1", "description": "Paseo CLI - control your AI coding agents from the command line", "bin": { "paseo": "bin/paseo" @@ -28,9 +28,9 @@ }, "dependencies": { "@clack/prompts": "^1.0.0", - "@getpaseo/client": "0.10.0", - "@getpaseo/protocol": "0.10.0", - "@getpaseo/server": "0.10.0", + "@getpaseo/client": "0.11.0-beta.1", + "@getpaseo/protocol": "0.11.0-beta.1", + "@getpaseo/server": "0.11.0-beta.1", "chalk": "^5.3.0", "commander": "^12.0.0", "mime-types": "^2.1.35", diff --git a/packages/client/package.json b/packages/client/package.json index 4e5a9124c8..2d092bb2e1 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/client", - "version": "0.10.0", + "version": "0.11.0-beta.1", "description": "Paseo client SDK package", "files": [ "dist", @@ -39,8 +39,8 @@ "test": "vitest run" }, "dependencies": { - "@getpaseo/protocol": "0.10.0", - "@getpaseo/relay": "0.10.0", + "@getpaseo/protocol": "0.11.0-beta.1", + "@getpaseo/relay": "0.11.0-beta.1", "zod": "^4.4.3" }, "devDependencies": { diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 9436a826df..265476e21c 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/desktop", - "version": "0.10.0", + "version": "0.11.0-beta.1", "private": true, "description": "Paseo desktop app (Electron wrapper)", "homepage": "https://paseo.sh", diff --git a/packages/expo-two-way-audio/package.json b/packages/expo-two-way-audio/package.json index 259bb95330..3b3434c510 100644 --- a/packages/expo-two-way-audio/package.json +++ b/packages/expo-two-way-audio/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/expo-two-way-audio", - "version": "0.10.0", + "version": "0.11.0-beta.1", "description": "Native module for two way audio streaming", "keywords": [ "ExpoTwoWayAudio", diff --git a/packages/highlight/package.json b/packages/highlight/package.json index d94d404079..5fd1c63f82 100644 --- a/packages/highlight/package.json +++ b/packages/highlight/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/highlight", - "version": "0.10.0", + "version": "0.11.0-beta.1", "files": [ "dist", "!dist/**/*.map", diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 19cf0cb075..74b6e7a91d 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/plugin", - "version": "0.10.0", + "version": "0.11.0-beta.1", "description": "Paseo plugin SDK package", "files": [ "dist", @@ -70,8 +70,8 @@ "use-sync-external-store": "^1.6.0" }, "devDependencies": { - "@getpaseo/client": "0.10.0", - "@getpaseo/protocol": "0.10.0", + "@getpaseo/client": "0.11.0-beta.1", + "@getpaseo/protocol": "0.11.0-beta.1", "@types/node": "^20.9.0", "@types/react": "~19.2.0", "react": "19.1.0", @@ -81,8 +81,8 @@ "zod": "^4.4.3" }, "peerDependencies": { - "@getpaseo/client": "0.10.0", - "@getpaseo/protocol": "0.10.0", + "@getpaseo/client": "0.11.0-beta.1", + "@getpaseo/protocol": "0.11.0-beta.1", "react": "~19.1.0", "react-native": ">=0.81.5", "zod": "^4.4.3" diff --git a/packages/protocol/package.json b/packages/protocol/package.json index 4d07048229..8e9e6f3e21 100644 --- a/packages/protocol/package.json +++ b/packages/protocol/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/protocol", - "version": "0.10.0", + "version": "0.11.0-beta.1", "description": "Paseo shared protocol schemas and wire types", "files": [ "dist", diff --git a/packages/relay/package.json b/packages/relay/package.json index 10185476e3..5789910648 100644 --- a/packages/relay/package.json +++ b/packages/relay/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/relay", - "version": "0.10.0", + "version": "0.11.0-beta.1", "description": "Paseo relay for bridging daemon and client connections", "files": [ "dist", diff --git a/packages/server/package.json b/packages/server/package.json index e3c5562035..a2c1506707 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/server", - "version": "0.10.0", + "version": "0.11.0-beta.1", "description": "Paseo backend server", "files": [ "dist/server", @@ -107,11 +107,11 @@ "@agentclientprotocol/sdk": "^0.17.1", "@anthropic-ai/claude-agent-sdk": "0.3.246", "@anthropic-ai/sdk": "^0.104.2", - "@getpaseo/client": "0.10.0", - "@getpaseo/highlight": "0.10.0", - "@getpaseo/plugin": "0.10.0", - "@getpaseo/protocol": "0.10.0", - "@getpaseo/relay": "0.10.0", + "@getpaseo/client": "0.11.0-beta.1", + "@getpaseo/highlight": "0.11.0-beta.1", + "@getpaseo/plugin": "0.11.0-beta.1", + "@getpaseo/protocol": "0.11.0-beta.1", + "@getpaseo/relay": "0.11.0-beta.1", "@isaacs/ttlcache": "^2.1.4", "@modelcontextprotocol/sdk": "1.29.0", "@opencode-ai/sdk": "1.14.46", diff --git a/packages/website/package.json b/packages/website/package.json index de616b3507..f59e082539 100644 --- a/packages/website/package.json +++ b/packages/website/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/website", - "version": "0.10.0", + "version": "0.11.0-beta.1", "private": true, "type": "module", "scripts": { diff --git a/plugins/package.json b/plugins/package.json index 8f1e447aca..8182ce1bda 100644 --- a/plugins/package.json +++ b/plugins/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/builtin-plugins", - "version": "0.0.0", + "version": "0.11.0-beta.1", "private": true, "type": "module", "scripts": { From 37b66897a92bd8aa091b7efcd9050c493ef910a6 Mon Sep 17 00:00:00 2001 From: "paseo-ai[bot]" <266920839+paseo-ai[bot]@users.noreply.github.com> Date: Thu, 1 Oct 2026 08:59:09 +0000 Subject: [PATCH 045/100] fix: update lockfile signatures and Nix hash [skip ci] --- nix/npm-deps.hash | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nix/npm-deps.hash b/nix/npm-deps.hash index 8706078489..906f4c5f7f 100644 --- a/nix/npm-deps.hash +++ b/nix/npm-deps.hash @@ -1 +1 @@ -sha256-pVwIsufl/4VVcTiqJYbqy4hNnL2O5EBpl5DN5As+20c= +sha256-wS6J8HSLqxaFmdX849bowzUkTnPueRM61rcdcvFEUuQ= From 95774ba71fda2ec48e8cccaa58a4641936d6e726 Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Thu, 1 Oct 2026 12:04:05 +0200 Subject: [PATCH 046/100] Show each Paseo skill once in Codex (#5827) * fix(skills): install one Codex-discoverable orchestration copy * fix(skills): keep installed skills when agent skill roots are symlinked together Retiring the old Codex copy deleted the skills just installed when ~/.codex/skills resolved to ~/.agents/skills or ~/.claude/skills (or the reverse), and status reported drift forever. The Codex skill directory is now treated as a leftover only when it is a separate directory from the shared and Claude copies. Co-authored-by: 3ae3ae * test(e2e): expect agent skills in the shared and Claude roots only The skills settings spec still expected every skill in ~/.codex/skills. It now checks the two install roots and that ~/.codex/skills holds no Paseo skill, which fails against the previous installer. --------- Co-authored-by: 3ae3ae Co-authored-by: Bae Seongmin <83870598+3ae3ae@users.noreply.github.com> Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> --- docs/data-model.md | 6 +- .../app/e2e/support/helpers/agent-skills.ts | 7 +- .../internal/controller.test.ts | 17 +-- .../internal/operations.test.ts | 107 ++++++++++++++++-- .../internal/operations.ts | 28 ++++- .../internal/sync.test.ts | 17 +-- .../orchestration-skills/internal/sync.ts | 56 ++++++++- 7 files changed, 199 insertions(+), 39 deletions(-) diff --git a/docs/data-model.md b/docs/data-model.md index 8c63e67d0b..25a214b11c 100644 --- a/docs/data-model.md +++ b/docs/data-model.md @@ -182,8 +182,10 @@ Terminal activity contributes to the workspace status bucket **per `workspaceId` Single file, validated with `PersistedConfigSchema`. `agents.skills.selection` is the daemon host's orchestration-skill preference. Missing means -`{ mode: "all" }`. Installed state is not persisted; the daemon derives it from its three managed -skill directories and keeps config plus filesystem convergence behind one serialized owner. +`{ mode: "all" }`. Installed state is not persisted; the daemon derives it from the shared +`~/.agents/skills` and Claude skill directories and keeps config plus filesystem convergence behind +one serialized owner. Codex discovers the shared directory. Updates retire unchanged files from +Paseo-managed copies in `~/.codex/skills`, preserving user edits and untracked files. `paseo reload` reads and validates this file once inside the daemon. That snapshot drives resolution, classification, application, and reload bookkeeping. `DaemonConfigStore` owns applying runtime-safe diff --git a/packages/app/e2e/support/helpers/agent-skills.ts b/packages/app/e2e/support/helpers/agent-skills.ts index 76c7a56d4a..5d8c15633c 100644 --- a/packages/app/e2e/support/helpers/agent-skills.ts +++ b/packages/app/e2e/support/helpers/agent-skills.ts @@ -22,7 +22,7 @@ type AgentSkillsClient = Pick< "close" | "connect" | "getAgentSkillsStatus" | "saveAgentSkillsSelection" >; -export type SkillTargetName = "agents" | "claude" | "codex"; +export type SkillTargetName = "agents" | "claude"; export interface AgentSkillsSandbox { available: string[]; @@ -30,6 +30,7 @@ export interface AgentSkillsSandbox { daemon: IsolatedHostDaemon; home: string; targets: Record; + codexSkillsDir: string; blockAgentsDirectory(): Promise; unblockAgentsDirectory(): Promise; close(): Promise; @@ -63,8 +64,8 @@ export async function startAgentSkillsSandbox(): Promise { const targets = { agents: path.join(home, ".agents", "skills"), claude: path.join(home, ".claude", "skills"), - codex: path.join(home, ".codex", "skills"), }; + const codexSkillsDir = path.join(home, ".codex", "skills"); return { available, @@ -72,6 +73,7 @@ export async function startAgentSkillsSandbox(): Promise { daemon, home, targets, + codexSkillsDir, blockAgentsDirectory: async () => { await mkdir(path.dirname(targets.agents), { recursive: true }); await writeFile(targets.agents, "not a directory", "utf8"); @@ -196,6 +198,7 @@ export async function expectInstalledSkills( for (const directory of Object.values(sandbox.targets)) { await expect.poll(() => readInstalledSkills(directory)).toEqual([...expected].sort()); } + await expect.poll(() => readInstalledSkills(sandbox.codexSkillsDir)).toEqual([]); } export async function expectSavedSelection( diff --git a/packages/server/src/server/orchestration-skills/internal/controller.test.ts b/packages/server/src/server/orchestration-skills/internal/controller.test.ts index d3dab5aa78..23b541e67d 100644 --- a/packages/server/src/server/orchestration-skills/internal/controller.test.ts +++ b/packages/server/src/server/orchestration-skills/internal/controller.test.ts @@ -210,7 +210,7 @@ async function blockAgentsDir(targets: SkillTargets): Promise { } async function isInstalled(targets: SkillTargets, name: string): Promise { - const dirs = [targets.agentsDir, targets.claudeDir, targets.codexDir]; + const dirs = [targets.agentsDir, targets.claudeDir]; const present = await Promise.all( dirs.map((dir) => access(path.join(dir, name)) @@ -454,7 +454,7 @@ describe("skills controller", () => { expect(await installedEverywhere(readOnly.targets)).toEqual([ ["paseo", "paseo-loop"], ["paseo", "paseo-loop"], - ["paseo", "paseo-loop"], + ["paseo-loop"], ]); expect(await readUserFile(readOnly.targets, "paseo-loop", "notes/mine.md")).toEqual([ "hand written", @@ -575,6 +575,7 @@ describe("skills controller", () => { }; const next: SkillSelection = { mode: "custom", skills: ["paseo"] }; await harness.controller.save(previous); + await writeUserFile(harness.targets, "paseo-loop", "SKILL.md", "paseo-loop-v1"); const livePaths = [ harness.targets.agentsDir, harness.targets.claudeDir, @@ -601,6 +602,7 @@ describe("skills controller", () => { }; const next: SkillSelection = { mode: "custom", skills: ["paseo"] }; await harness.controller.save(previous); + await writeUserFile(harness.targets, "paseo-loop", "SKILL.md", "paseo-loop-v1"); const transaction = await beginSkillsTransaction(harness.targets, previous, next, [ { kind: "delete", name: "paseo-loop" }, @@ -666,6 +668,7 @@ describe("skills controller", () => { }; const next: SkillSelection = { mode: "custom", skills: ["paseo"] }; await harness.controller.save(previous); + await writeUserFile(harness.targets, "paseo-loop", "SKILL.md", "paseo-loop-v1"); await writeUserFile(harness.targets, "paseo-loop", "notes/mine.md", "staged notes"); const transaction = await beginSkillsTransaction(harness.targets, previous, next, [ @@ -1115,7 +1118,7 @@ describe("skills controller", () => { expect(await installedEverywhere(harness.targets)).toEqual([ ["paseo", "paseo-advisor", "paseo-loop"], ["paseo", "paseo-advisor", "paseo-loop"], - ["paseo", "paseo-advisor", "paseo-loop"], + ["paseo-advisor"], ]); expect(result.selection).toEqual({ mode: "custom", skills: ["paseo", "paseo-loop"] }); }); @@ -1131,7 +1134,7 @@ describe("skills controller", () => { expect(result.confirmationRequired).toBeNull(); expect(result.selection).toEqual({ mode: "custom", skills: ["paseo"] }); - expect(await installedEverywhere(harness.targets)).toEqual([["paseo"], ["paseo"], ["paseo"]]); + expect(await installedEverywhere(harness.targets)).toEqual([["paseo"], ["paseo"], []]); }); it("asks again when another directory appears before the retry", async () => { @@ -1150,7 +1153,7 @@ describe("skills controller", () => { expect(await installedEverywhere(harness.targets)).toEqual([ ["paseo", "paseo-advisor", "paseo-chat", "paseo-loop"], ["paseo", "paseo-advisor", "paseo-chat", "paseo-loop"], - ["paseo", "paseo-advisor", "paseo-chat", "paseo-loop"], + ["paseo-chat"], ]); }); @@ -1179,7 +1182,7 @@ describe("skills controller", () => { const result = await harness.controller.save({ mode: "custom", skills: ["paseo"] }); expect(result.confirmationRequired).toBeNull(); - expect(await installedEverywhere(harness.targets)).toEqual([["paseo"], ["paseo"], ["paseo"]]); + expect(await installedEverywhere(harness.targets)).toEqual([["paseo"], ["paseo"], []]); }); it("preserves a regular file at a skill path when save convergence fails", async () => { @@ -1211,7 +1214,7 @@ describe("skills controller", () => { ]); expect(saved.selection).toEqual({ mode: "custom", skills: ["paseo"] }); - expect(await installedEverywhere(harness.targets)).toEqual([["paseo"], ["paseo"], ["paseo"]]); + expect(await installedEverywhere(harness.targets)).toEqual([["paseo"], ["paseo"], []]); expect(await harness.controller.status()).toEqual({ state: "up-to-date", ops: [], diff --git a/packages/server/src/server/orchestration-skills/internal/operations.test.ts b/packages/server/src/server/orchestration-skills/internal/operations.test.ts index 6b188d24b0..14363c4b42 100644 --- a/packages/server/src/server/orchestration-skills/internal/operations.test.ts +++ b/packages/server/src/server/orchestration-skills/internal/operations.test.ts @@ -1,4 +1,5 @@ import { promises as fs } from "node:fs"; +import { createHash } from "node:crypto"; import os from "node:os"; import path from "node:path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; @@ -298,8 +299,8 @@ describe("custom skill selection", () => { available: ["paseo", "paseo-advisor", "paseo-loop"], installed: ["paseo", "paseo-loop"], }); - expect(await installedIn(sandbox.targets, "paseo")).toEqual([true, true, true]); - expect(await installedIn(sandbox.targets, "paseo-loop")).toEqual([true, true, true]); + expect(await installedIn(sandbox.targets, "paseo")).toEqual([true, true, false]); + expect(await installedIn(sandbox.targets, "paseo-loop")).toEqual([true, true, false]); expect(await installedIn(sandbox.targets, "paseo-advisor")).toEqual([false, false, false]); }); @@ -322,7 +323,7 @@ describe("custom skill selection", () => { const status = await installSkills(sandbox.targets, only("paseo")); expect(status.state).toBe("up-to-date"); - expect(await installedIn(sandbox.targets, "paseo")).toEqual([true, true, true]); + expect(await installedIn(sandbox.targets, "paseo")).toEqual([true, true, false]); expect(await installedIn(sandbox.targets, "paseo-loop")).toEqual([false, false, false]); expect(await installedIn(sandbox.targets, "paseo-advisor")).toEqual([false, false, false]); }); @@ -395,7 +396,7 @@ describe("installSkills / updateSkills", () => { await fs.rm(sandbox.root, { recursive: true, force: true }); }); - it("installs from a clean machine, populates all three targets, and leaves user dirs alone", async () => { + it("installs into shared and Claude roots without duplicating Codex discovery", async () => { await writeCurrentBundle(sandbox.targets.sourceDir); await writeOnDiskSkill(sandbox.targets.agentsDir, "unslop", { "SKILL.md": "user-unslop" }); @@ -411,9 +412,7 @@ describe("installSkills / updateSkills", () => { expect( await fs.readFile(path.join(sandbox.targets.agentsDir, name, "SKILL.md"), "utf-8"), ).toBe(name === "paseo" ? "paseo-v1" : "loop-v1"); - expect( - await fs.readFile(path.join(sandbox.targets.codexDir, name, "SKILL.md"), "utf-8"), - ).toBe(name === "paseo" ? "paseo-v1" : "loop-v1"); + expect(await pathExists(path.join(sandbox.targets.codexDir, name, "SKILL.md"))).toBe(false); expect(await pathExists(path.join(sandbox.targets.claudeDir, name))).toBe(true); } expect( @@ -421,6 +420,100 @@ describe("installSkills / updateSkills", () => { ).toBe("user-unslop"); }); + it("retires unchanged managed Codex copies while preserving personal files", async () => { + await writeCurrentBundle(sandbox.targets.sourceDir); + await installSkills(sandbox.targets, ALL_SKILLS); + const legacy = path.join(sandbox.targets.codexDir, "paseo"); + const managed = { "SKILL.md": "paseo-v1", "references/old.md": "bundled reference" }; + const hashes = Object.fromEntries( + Object.entries(managed).map(([name, content]) => [ + name, + createHash("sha256").update(content).digest("hex"), + ]), + ); + await writeFiles(legacy, { + ...managed, + "notes/mine.md": "personal notes", + ".paseo-managed-files.json": JSON.stringify({ version: 1, files: hashes }), + }); + const status = await autoUpdateInstalledSkills(sandbox.targets, ALL_SKILLS); + expect(status.state).toBe("up-to-date"); + expect(status.ops).toEqual([]); + expect(await pathExists(path.join(legacy, "SKILL.md"))).toBe(false); + expect(await pathExists(path.join(legacy, "references/old.md"))).toBe(false); + expect(await fs.readFile(path.join(legacy, "notes/mine.md"), "utf8")).toBe("personal notes"); + expect(await autoUpdateInstalledSkills(sandbox.targets, ALL_SKILLS)).toEqual(status); + }); + + it("keeps edited or untracked Codex skill definitions", async () => { + await writeCurrentBundle(sandbox.targets.sourceDir); + await installSkills(sandbox.targets, ALL_SKILLS); + const originalHash = createHash("sha256").update("paseo-v1").digest("hex"); + await writeOnDiskSkill(sandbox.targets.codexDir, "paseo", { + "SKILL.md": "personalized skill", + ".paseo-managed-files.json": JSON.stringify({ + version: 1, + files: { "SKILL.md": originalHash }, + }), + }); + await writeOnDiskSkill(sandbox.targets.codexDir, "paseo-loop", { + "SKILL.md": "untracked skill", + }); + expect((await autoUpdateInstalledSkills(sandbox.targets, ALL_SKILLS)).state).toBe("up-to-date"); + expect( + await fs.readFile(path.join(sandbox.targets.codexDir, "paseo", "SKILL.md"), "utf8"), + ).toBe("personalized skill"); + expect( + await fs.readFile(path.join(sandbox.targets.codexDir, "paseo-loop", "SKILL.md"), "utf8"), + ).toBe("untracked skill"); + }); + + it.each([ + { alias: "codex", target: "agents" }, + { alias: "codex", target: "claude" }, + { alias: "claude", target: "codex" }, + ] as const)( + "keeps installed skills when the $alias root links to the $target root", + async ({ alias, target }) => { + await writeCurrentBundle(sandbox.targets.sourceDir); + const rootOf = { agents: "agentsDir", claude: "claudeDir", codex: "codexDir" } as const; + const aliasRoot = sandbox.targets[rootOf[alias]]; + const targetRoot = sandbox.targets[rootOf[target]]; + await fs.mkdir(targetRoot, { recursive: true }); + await fs.mkdir(path.dirname(aliasRoot), { recursive: true }); + await fs.symlink(targetRoot, aliasRoot, "dir"); + + const installed = await installSkills(sandbox.targets, ALL_SKILLS); + const updated = await autoUpdateInstalledSkills(sandbox.targets, ALL_SKILLS); + + expect(installed.state).toBe("up-to-date"); + expect(updated.state).toBe("up-to-date"); + for (const name of ["paseo", "paseo-loop"]) { + expect(await installedIn(sandbox.targets, name)).toEqual([true, true, true]); + } + }, + ); + + it("keeps an installed skill when its Codex directory links to the shared copy", async () => { + await writeCurrentBundle(sandbox.targets.sourceDir); + await fs.mkdir(path.join(sandbox.targets.agentsDir, "paseo"), { recursive: true }); + await fs.mkdir(sandbox.targets.codexDir, { recursive: true }); + await fs.symlink( + path.join(sandbox.targets.agentsDir, "paseo"), + path.join(sandbox.targets.codexDir, "paseo"), + "dir", + ); + + const installed = await installSkills(sandbox.targets, ALL_SKILLS); + const updated = await autoUpdateInstalledSkills(sandbox.targets, ALL_SKILLS); + + expect(installed.state).toBe("up-to-date"); + expect(updated.state).toBe("up-to-date"); + expect( + await fs.readFile(path.join(sandbox.targets.agentsDir, "paseo", "SKILL.md"), "utf8"), + ).toBe("paseo-v1"); + }); + it("repairs missing and edited skills without deleting a legacy directory", async () => { await writeCurrentBundle(sandbox.targets.sourceDir); await writeOnDiskSkill(sandbox.targets.agentsDir, "paseo", { "SKILL.md": "stale" }); diff --git a/packages/server/src/server/orchestration-skills/internal/operations.ts b/packages/server/src/server/orchestration-skills/internal/operations.ts index a3b0831122..e45878a953 100644 --- a/packages/server/src/server/orchestration-skills/internal/operations.ts +++ b/packages/server/src/server/orchestration-skills/internal/operations.ts @@ -2,7 +2,13 @@ import { createHash } from "node:crypto"; import { promises as fs } from "node:fs"; import path from "node:path"; import type { AgentSkillSelection } from "@getpaseo/protocol/messages"; -import { listFilesRecursive, removeSkill, syncSkills } from "./sync.js"; +import { + isLegacyCodexCopy, + listFilesRecursive, + readManagedFilesManifest, + removeSkill, + syncSkills, +} from "./sync.js"; export type SkillsState = "not-installed" | "up-to-date" | "drift"; @@ -163,11 +169,23 @@ export async function getSkillsStatus( hashSkills(targets.claudeDir, names), hashSkills(targets.codexDir, names), ]); - const disks = [agentsDisk, claudeDisk, codexDisk]; - const ops = diff(bundle, disks, names, resolveDesiredSkills(selection, available)); - const installed = installedSkillNames(disks, names); + const disks = [agentsDisk, claudeDisk]; + const desired = resolveDesiredSkills(selection, available); + const ops = diff(bundle, disks, names, desired); + for (const name of codexDisk.keys()) { + if (ops.some((op) => op.name === name)) continue; + if (!(await isLegacyCodexCopy(name, targets))) continue; + if (!desired.has(name)) ops.push({ kind: "delete", name }); + else if (await readManagedFilesManifest(path.join(targets.codexDir, name))) { + ops.push({ kind: "update", name }); + } + } + ops.sort((a, b) => compareStrings(a.name, b.name)); + const allDisks = [...disks, codexDisk]; + const installed = installedSkillNames(allDisks, names); - if (!hasInstalledPaseoSkill(disks)) return { state: "not-installed", ops, available, installed }; + if (!hasInstalledPaseoSkill(allDisks)) + return { state: "not-installed", ops, available, installed }; if (ops.length === 0) return { state: "up-to-date", ops, available, installed }; return { state: "drift", ops, available, installed }; } diff --git a/packages/server/src/server/orchestration-skills/internal/sync.test.ts b/packages/server/src/server/orchestration-skills/internal/sync.test.ts index da5b1c8765..1848330fdb 100644 --- a/packages/server/src/server/orchestration-skills/internal/sync.test.ts +++ b/packages/server/src/server/orchestration-skills/internal/sync.test.ts @@ -75,11 +75,9 @@ describe("syncSkills", () => { "utf-8", ); expect(claudeContent).toBe("new paseo content"); - const codexContent = await fs.readFile( - path.join(sandbox.codexDir, "paseo", "SKILL.md"), - "utf-8", - ); - expect(codexContent).toBe("new paseo content"); + await expect(fs.stat(path.join(sandbox.codexDir, "paseo", "SKILL.md"))).rejects.toMatchObject({ + code: "ENOENT", + }); }); it("installs new bundled skills, including references/, when not present on disk", async () => { @@ -105,12 +103,9 @@ describe("syncSkills", () => { "utf-8", ), ).toBe("roles content"); - expect( - await fs.readFile( - path.join(sandbox.codexDir, "paseo-committee", "references", "roles.md"), - "utf-8", - ), - ).toBe("roles content"); + await expect(fs.stat(path.join(sandbox.codexDir, "paseo-committee"))).rejects.toMatchObject({ + code: "ENOENT", + }); const claudeSkillDir = path.join(sandbox.claudeDir, "paseo-committee"); expect((await fs.lstat(claudeSkillDir)).isDirectory()).toBe(true); diff --git a/packages/server/src/server/orchestration-skills/internal/sync.ts b/packages/server/src/server/orchestration-skills/internal/sync.ts index 9233365549..255c61a274 100644 --- a/packages/server/src/server/orchestration-skills/internal/sync.ts +++ b/packages/server/src/server/orchestration-skills/internal/sync.ts @@ -51,7 +51,9 @@ export async function listFilesRecursive(rootDir: string): Promise { return out; } -async function readManagedFilesManifest(dstDir: string): Promise { +export async function readManagedFilesManifest( + dstDir: string, +): Promise { const raw = await fs .readFile(path.join(dstDir, MANAGED_FILES_MANIFEST), "utf-8") .catch(() => null); @@ -185,6 +187,50 @@ export async function removeSkill(skillName: string, targets: RemoveSkillTargets } } +async function isSameDirectory(a: string, b: string): Promise { + const [realA, realB] = await Promise.all([a, b].map((p) => fs.realpath(p).catch(() => null))); + return realA !== null && realA === realB; +} + +/** + * Codex also discovers .agents/skills, so its dedicated copy is a leftover. When + * the Codex skill directory resolves to the shared or Claude copy, it is that + * copy, and retiring it would delete the install. + */ +export async function isLegacyCodexCopy( + skillName: string, + targets: RemoveSkillTargets, +): Promise { + const codexSkillDir = path.join(targets.codexDir, skillName); + for (const root of [targets.agentsDir, targets.claudeDir]) { + if (await isSameDirectory(codexSkillDir, path.join(root, skillName))) return false; + } + return true; +} + +async function retireManagedSkill(dstDir: string): Promise { + const info = await fs.lstat(dstDir).catch(() => null); + if (!info?.isDirectory()) return 0; + const manifest = await readManagedFilesManifest(dstDir); + if (!manifest) return 0; + const files = Object.keys(manifest.files); + await assertManagedPathsStayInsideSkill(dstDir, [MANAGED_FILES_MANIFEST, ...files]); + const removed: string[] = []; + for (const [rel, previousHash] of Object.entries(manifest.files)) { + const filePath = path.join(dstDir, rel); + const currentHash = await hashFile(filePath).catch(() => null); + if (currentHash !== previousHash) continue; + await fs.rm(filePath); + removed.push(rel); + } + await pruneEmptyParentDirs(dstDir, removed); + await fs.rm(path.join(dstDir, MANAGED_FILES_MANIFEST)); + await fs.rmdir(dstDir).catch((error: NodeJS.ErrnoException) => { + if (error.code !== "ENOTEMPTY" && error.code !== "ENOENT") throw error; + }); + return removed.length + 1; +} + export async function syncSkills(options: SkillSyncOptions): Promise { let changedFiles = 0; let processedSkills = 0; @@ -206,10 +252,10 @@ export async function syncSkills(options: SkillSyncOptions): Promise Date: Thu, 1 Oct 2026 14:02:49 +0200 Subject: [PATCH 047/100] Let plugin client code import packages that declare only main (#5838) The plugin compiler builds client code with esbuild's neutral platform, which reads no package.json entry fields, so any dependency without an exports map failed with Could not resolve. Read module and main for the client target, as the server target already does through the node platform. Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> --- .../src/server/plugins/compiler.test.ts | 27 +++++++++++++++++++ .../server/src/server/plugins/compiler.ts | 4 +++ 2 files changed, 31 insertions(+) diff --git a/packages/server/src/server/plugins/compiler.test.ts b/packages/server/src/server/plugins/compiler.test.ts index dca7d3e3e2..facba48fdb 100644 --- a/packages/server/src/server/plugins/compiler.test.ts +++ b/packages/server/src/server/plugins/compiler.test.ts @@ -455,6 +455,33 @@ export type Value = string;`, expect(serverBundle).not.toContain("Client contribution"); }); + it.each(["main", "module"])( + "builds a dependency that declares only %s into both runtimes", + async (field) => { + const entries = await createSplitPlugin(); + const dependency = path.join(entries.directory, "node_modules/legacy-entry"); + await mkdir(dependency, { recursive: true }); + await writeFile( + path.join(dependency, "package.json"), + JSON.stringify({ name: "legacy-entry", [field]: "lib/entry.js" }), + ); + await mkdir(path.join(dependency, "lib")); + await writeFile( + path.join(dependency, "lib/entry.js"), + 'exports.label = "legacy entry contribution";', + ); + await writeFile( + path.join(entries.directory, "shared/labels.ts"), + 'import { label } from "legacy-entry"; export const clientLabel = label; export const serverLabel = label;', + ); + + const { clientBundle, serverBundle } = await compilePlugin(entries); + + expect(clientBundle).toContain("legacy entry contribution"); + expect(serverBundle).toContain("legacy entry contribution"); + }, + ); + it("uses the automatic JSX runtime without a React import", async () => { const entries = await createSplitPlugin(); const { clientBundle } = await compilePlugin(entries); diff --git a/packages/server/src/server/plugins/compiler.ts b/packages/server/src/server/plugins/compiler.ts index 80dbb1b7b0..15df395d06 100644 --- a/packages/server/src/server/plugins/compiler.ts +++ b/packages/server/src/server/plugins/compiler.ts @@ -391,6 +391,10 @@ async function compileTarget(entryPath: string, target: PluginBuildTarget): Prom jsx: "automatic", platform: target === "server" ? "node" : "neutral", target: target === "server" ? "node20" : "es2020", + // The neutral platform reads no package.json entry fields, so packages without + // `exports` would not resolve. Read the same fields as esbuild's browser platform + // minus `browser`, since the bundle also runs in React Native. + mainFields: target === "client" ? ["module", "main"] : undefined, // Metro lowers async syntax before Hermes sees app code. Plugin client bundles bypass Metro, // so apply the same compatibility transform before the app evaluates them from source. supported: target === "client" ? { "async-await": false } : undefined, From 70df09daeb7cc57c8777b965731f6c68d863a4b6 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Thu, 1 Oct 2026 14:51:11 +0100 Subject: [PATCH 048/100] Restore desktop usage and subscription discovery across harnesses (#5844) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(app): hide the sidebar Usage item while it has no summary Without a summary window with data, the item fell back to a plain "Usage" row that duplicated the footer's Usage icon. It now renders nothing, and the footer rows leave it out so an otherwise empty rows block and its separator disappear with it. * fix(desktop): restore built-in usage in packaged apps The external plugin compiler cannot traverse app.asar, and dependency packaging drops declarations needed by import validation. Ship complete built-in resources beside the archive and verify registration in the packaged desktop smoke check. * fix(usage): restore login discovery across harnesses Discover subscription accounts independently of agent sessions and retain each account’s logins in preference order so an unusable credential can fall back to another login for the same account. --- .github/workflows/desktop-packages.yml | 1 + docs/glossary.md | 2 +- docs/plugins.md | 11 +- .../e2e/browser/plugin-sidebar-items.spec.ts | 4 + .../e2e/browser/sidebar-nav-settings.spec.ts | 2 + packages/app/e2e/browser/usage-screen.spec.ts | 3 +- .../e2e/browser/usage-sidebar-item.spec.ts | 32 ++ .../support/helpers/sidebar-nav-settings.ts | 1 - .../e2e/support/helpers/usage-sidebar-item.ts | 10 +- packages/app/src/components/left-sidebar.tsx | 12 +- packages/app/src/usage/index.ts | 2 +- packages/app/src/usage/queries.ts | 18 +- packages/app/src/usage/sidebar-item.tsx | 84 ++---- packages/desktop/e2e/packaged-app-smoke.js | 27 ++ packages/desktop/electron-builder.yml | 4 + .../src/server/plugins/builtin/index.ts | 6 + .../server/plugins/usage-sources.e2e.test.ts | 223 +++++++++++++- .../plugins/usage-sources/index.test.ts | 86 ++++++ .../src/server/plugins/usage-sources/index.ts | 74 +++-- plugins/claude-usage-source/index.server.ts | 4 +- .../server/keychain.test.ts | 44 ++- .../claude-usage-source/server/stores.test.ts | 277 ++++++++++++++++++ plugins/claude-usage-source/server/stores.ts | 132 +++++++++ .../claude-usage-source/server/usage.test.ts | 112 ++++--- plugins/claude-usage-source/server/usage.ts | 74 +++-- plugins/claude-usage-source/shared/input.ts | 14 +- plugins/codex-usage-source/index.server.ts | 4 +- .../codex-usage-source/server/stores.test.ts | 245 ++++++++++++++++ plugins/codex-usage-source/server/stores.ts | 132 +++++++++ .../codex-usage-source/server/usage.test.ts | 60 ++-- plugins/codex-usage-source/server/usage.ts | 80 +++-- plugins/codex-usage-source/shared/input.ts | 12 +- public-docs/plugins/reference.md | 11 + 33 files changed, 1580 insertions(+), 223 deletions(-) create mode 100644 plugins/claude-usage-source/server/stores.test.ts create mode 100644 plugins/claude-usage-source/server/stores.ts create mode 100644 plugins/codex-usage-source/server/stores.test.ts create mode 100644 plugins/codex-usage-source/server/stores.ts diff --git a/.github/workflows/desktop-packages.yml b/.github/workflows/desktop-packages.yml index e2f1fafb3c..c30eea7da4 100644 --- a/.github/workflows/desktop-packages.yml +++ b/.github/workflows/desktop-packages.yml @@ -9,6 +9,7 @@ on: branches: [main] paths: - "packages/desktop/**" + - "plugins/**" - "nix/**" - ".github/workflows/desktop-packages.yml" diff --git a/docs/glossary.md b/docs/glossary.md index 512068ece8..67730885fb 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -34,7 +34,7 @@ Authoritative terminology. UI label wins. Don't invent synonyms; use what's here - **Usage source** — Plugin registration that discovers accounts and fetches plan usage. Code: `UsageSourceRegistration` (`packages/plugin/src/server/usage.ts`). - **Report ID** — Stable daemon-composed identity for one source account: `:`. The source owns the account key; clients treat the whole ID as opaque. Code: `UsageReportEntry.id` (`packages/protocol/src/messages.ts`). - **Usage report** — A source's normalized usage for one account, including status and optional windows, balances, and details. Its entry carries the Report ID, account label, and fetch time. Code: `UsageReport` (`packages/plugin/src/server/usage.ts`). -- **Usage (sidebar item)** — The sidebar footer item showing each pinned window's source icon and percent, or a plain "Usage" row while no pinned window has data; pressing it opens the Usage screen (a bottom sheet on phones). A pin is `{ sourceId, windowId }` and shows once per account of that source. Pins and the used/remaining choice are one per-device preference (`AppSettings.usage`) that every usage surface follows. It reads the active workspace's host, else the first connected host with usage sources. Code: `packages/app/src/usage/sidebar-item.tsx`, `usage/pinned.ts`, `usage/preferences.ts`. +- **Usage (sidebar item)** — The sidebar footer item showing each pinned window's source icon and percent, hidden while no pinned window has data (the footer's Usage icon still opens the screen); pressing it opens the Usage screen (a bottom sheet on phones). A pin is `{ sourceId, windowId }` and shows once per account of that source. Pins and the used/remaining choice are one per-device preference (`AppSettings.usage`) that every usage surface follows. It reads the active workspace's host, else the first connected host with usage sources. Code: `packages/app/src/usage/sidebar-item.tsx`, `usage/pinned.ts`, `usage/preferences.ts`. - **Model** — A specific LLM offered by a provider. UI: "Model" / "Select model". Code: `AgentModelDefinition` (`packages/protocol/src/messages.ts:187`). - **Tab** — UI surface representing one session inside a workspace. Not a conceptual unit; use **Agent session** when talking about the model. Code: `WorkspaceTabDescriptor` (`packages/app/src/screens/workspace/workspace-tabs-types.ts`). - **Terminal** — Workspace-scoped PTY shell streamed over the binary mux channel. UI: "Terminal". Code: `TerminalStreamFrame` (`packages/protocol/src/terminal-stream-protocol.ts`). diff --git a/docs/plugins.md b/docs/plugins.md index b4471d10e7..ab12fe7dd2 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -93,6 +93,15 @@ Built-in plugins ship from `plugins//`, with `paseo-plugin.json`, `index.ser `packages/server/src/server/plugins/builtin/index.ts`; the workspace, build copy, and CI checks cover that registry. Unlisted directories do not load. +Desktop packaging ships the entire built-in plugin directory as an external resource, +including declarations. The external esbuild compiler cannot read Electron's `app.asar` +filesystem, and packaging dependencies excludes `.d.ts` files needed for import validation. +Built-ins can import only host modules (`@getpaseo/plugin/*`, `zod`) and Node built-ins: +outside the archive, nothing resolves an npm dependency, and the dist build test cannot catch +one because it resolves through the repository's `node_modules`. +The packaged-app smoke check requires every listed built-in to start without relying on +account credentials. + Built-ins run in process and remain active independently of `pluginsEnabled`. They are absent from the installed plugin list and source configuration; their client bundles appear in the plugin catalog. Editing one in development requires a daemon restart. @@ -464,7 +473,7 @@ SVG or URL. ## Usage sources -Register a usage source from `index.server.ts` with `server.registerUsageSource()`. Import `UsageSourceRegistration` and normalization helpers from `@getpaseo/plugin/server/usage`. `input` is a Zod schema checked inside the plugin process before `fetch(input)` runs. `discover()` returns the inputs for accounts found on the host; an empty list omits the source from host usage. `fetch()` returns a report with a stable `account.key` so the daemon can deduplicate accounts and cache by source and input. A failed input or fetch becomes an error report for that source. `icon` uses the same sanitized SVG file rules as provider icons. +Register a usage source from `index.server.ts` with `server.registerUsageSource()`. Import `UsageSourceRegistration` and normalization helpers from `@getpaseo/plugin/server/usage`. The plugin owns account discovery and credential-store reads; the daemon owns account grouping, ordered login fallback, and the fetch cache. Keep discovery independent of agent sessions and provider names: a harness can use a subscription through a proxy or renamed provider. Inputs are validated in the plugin process and remain daemon-side. `icon` uses the same sanitized SVG file rules as provider icons. The daemon calls discovery for `usage.list_reports`; the client gates this RPC on `server_info.features.usageSources`. The old `provider.usage.list` RPC maps discovered reports for older clients. See the [public usage source reference](../public-docs/plugins/reference.md#usage-sources) for the author contract and minimum version. diff --git a/packages/app/e2e/browser/plugin-sidebar-items.spec.ts b/packages/app/e2e/browser/plugin-sidebar-items.spec.ts index b36ea6ba98..49c781f84d 100644 --- a/packages/app/e2e/browser/plugin-sidebar-items.spec.ts +++ b/packages/app/e2e/browser/plugin-sidebar-items.spec.ts @@ -24,6 +24,8 @@ import { } from "../support/helpers/sidebar-nav-settings"; import { seedWorkspace } from "../support/helpers/seed-client"; import { getServerId } from "../support/helpers/server-id"; +import { installUsageReportsFixture } from "../support/helpers/usage-reports"; +import { claudeAndCodexReports } from "../support/helpers/usage-sidebar-item"; const APP_SETTINGS_KEY = "@paseo:app-settings"; @@ -152,6 +154,8 @@ test.describe("Plugin sidebar items", () => { page, }) => { test.setTimeout(180_000); + // The Usage item shows only with summary data. + await installUsageReportsFixture(page, { lists: [() => claudeAndCodexReports()] }); await page.setViewportSize(WIDE); await gotoWorkspace(page, workspaceId); const row = headerRow(page, SHOWCASE_PLUGIN_ID, "deploys"); diff --git a/packages/app/e2e/browser/sidebar-nav-settings.spec.ts b/packages/app/e2e/browser/sidebar-nav-settings.spec.ts index ae7919e952..499b67e0bf 100644 --- a/packages/app/e2e/browser/sidebar-nav-settings.spec.ts +++ b/packages/app/e2e/browser/sidebar-nav-settings.spec.ts @@ -184,6 +184,8 @@ test.describe("Sidebar footer rows in Appearance settings", () => { test("owner reorders and hides footer rows; the icon row stays fixed", async ({ page }) => { test.setTimeout(120_000); + // The Usage item shows only with summary data. + await installUsageReportsFixture(page, { lists: [() => claudeAndCodexReports()] }); // Keys for the fixed footer icon buttons are ignored. await seedSidebarFooterPreferences(page, [ { key: "help", visible: false }, diff --git a/packages/app/e2e/browser/usage-screen.spec.ts b/packages/app/e2e/browser/usage-screen.spec.ts index c7de5261aa..6512a8bd8f 100644 --- a/packages/app/e2e/browser/usage-screen.spec.ts +++ b/packages/app/e2e/browser/usage-screen.spec.ts @@ -184,7 +184,8 @@ test.describe("usage screen", () => { const usage = await installUsageReportsFixture(page, { usageSupported: false }); await gotoAppShell(page); - await page.locator('[data-testid="sidebar-usage"]:visible').first().click(); + // Without reports the footer has no Usage item; its Usage icon opens the screen. + await page.locator('[data-testid="sidebar-usage-icon"]:visible').first().click(); await expect( page.getByTestId(`usage-host-${serverId}`).getByText(/^Update .+ to see usage$/), diff --git a/packages/app/e2e/browser/usage-sidebar-item.spec.ts b/packages/app/e2e/browser/usage-sidebar-item.spec.ts index 124eaca5f6..0b1f04fed7 100644 --- a/packages/app/e2e/browser/usage-sidebar-item.spec.ts +++ b/packages/app/e2e/browser/usage-sidebar-item.spec.ts @@ -5,6 +5,7 @@ import { gotoAppShell, openSettings } from "../support/helpers/app"; import { getServerId } from "../support/helpers/server-id"; import { openSettingsHostSection } from "../support/helpers/settings"; import { + expectFooterSeparator, leaveSettings, openSidebarNavSettings, setFooterItemVisible, @@ -12,6 +13,7 @@ import { import { installUsageReportsFixture } from "../support/helpers/usage-reports"; import { claudeAndCodexReports, + expectNoUsageItem, expectOnUsageScreen, expectPinnedUsage, leaveUsageScreen, @@ -248,6 +250,36 @@ test.describe("Usage item", () => { }); }); +test("without summary data the footer drops the Usage item and keeps the Usage icon", async ({ + page, +}) => { + await installUsageReportsFixture(page, { + lists: [ + [ + { + id: "alpha:a", + account: {}, + fetchedAt: new Date().toISOString(), + sourceId: "alpha", + sourceLabel: "Alpha plan", + report: { status: "unavailable", windows: [] }, + }, + ], + ], + }); + await seedSidebarFooterPreferences(page, [{ key: "usage", visible: true }]); + await page.setViewportSize(WIDE); + await gotoAppShell(page); + await expect(page.locator('[data-testid="sidebar-usage-icon"]:visible')).toBeVisible({ + timeout: 30_000, + }); + await expectNoUsageItem(page); + await expectFooterSeparator(page, false); + await qaScreenshot(page, "desktop-footer-no-summary", { kind: "footer" }); + await page.locator('[data-testid="sidebar-usage-icon"]:visible').click(); + await expectOnUsageScreen(page); +}); + test("the Usage options show and hide the sidebar Usage item", async ({ page }) => { await installUsageReportsFixture(page, { lists: [() => claudeAndCodexReports()] }); await page.setViewportSize(WIDE); diff --git a/packages/app/e2e/support/helpers/sidebar-nav-settings.ts b/packages/app/e2e/support/helpers/sidebar-nav-settings.ts index 6631d839cb..c3ed13ea67 100644 --- a/packages/app/e2e/support/helpers/sidebar-nav-settings.ts +++ b/packages/app/e2e/support/helpers/sidebar-nav-settings.ts @@ -163,7 +163,6 @@ async function expectVerticalOrder( /** Persisted footer row key -> the testID the app shell renders that row with. */ function shellFooterTestID(key: string): string { - // With nothing pinned the Usage item is the plain Usage row. if (key === "usage") return "sidebar-usage"; const [, pluginId, itemId] = key.split(":"); return `plugin-sidebar-footer-${pluginId}-${itemId}`; diff --git a/packages/app/e2e/support/helpers/usage-sidebar-item.ts b/packages/app/e2e/support/helpers/usage-sidebar-item.ts index 56d4455099..7f73ec7897 100644 --- a/packages/app/e2e/support/helpers/usage-sidebar-item.ts +++ b/packages/app/e2e/support/helpers/usage-sidebar-item.ts @@ -137,7 +137,7 @@ function visible(page: Page, testID: string): Locator { return page.locator(`[data-testid="${testID}"]:visible`).first(); } -/** The sidebar footer's Usage item: pinned windows, or a plain "Usage" row without any. */ +/** The sidebar footer's Usage item: each summary window with data. */ export function usageItem(page: Page): Locator { return visible(page, "sidebar-usage"); } @@ -157,10 +157,10 @@ export async function expectPinnedUsage(page: Page, windows: string[]): Promise< await expect(pinned).toHaveText(windows); } -/** Without pinned windows the Usage item is a plain row that reads "Usage". */ -export async function expectNoPinnedUsage(page: Page): Promise { - await expect(page.locator('[data-testid="sidebar-usage-pinned-window"]:visible')).toHaveCount(0); - await expect(usageItem(page)).toHaveText("Usage"); +/** Without a summary window with data the footer has no Usage item, only the Usage icon. */ +export async function expectNoUsageItem(page: Page): Promise { + await expect(page.locator('[data-testid="sidebar-usage"]:visible')).toHaveCount(0); + await expect(page.locator('[data-testid="sidebar-usage-icon"]:visible')).toBeVisible(); } /** A window row, which is itself the pin toggle: "Claude", "Session". */ diff --git a/packages/app/src/components/left-sidebar.tsx b/packages/app/src/components/left-sidebar.tsx index 8095497ec7..0737634a1b 100644 --- a/packages/app/src/components/left-sidebar.tsx +++ b/packages/app/src/components/left-sidebar.tsx @@ -52,7 +52,7 @@ import { useCloseAgentListGesture } from "@/mobile-panels/gestures"; import { MobilePanelOverlay } from "@/mobile-panels/presentation"; import { buildSettingsAddHostRoute, buildSettingsRoute } from "@/utils/host-routes"; import { openHostOverview } from "@/navigation/settings-navigation"; -import { UsageSidebarItem, useOpenUsageScreen } from "@/usage"; +import { UsageSidebarItem, useHasUsageSummary, useOpenUsageScreen } from "@/usage"; import { SidebarAgentListSkeleton } from "./sidebar-agent-list-skeleton"; import { SidebarCalloutSlot } from "./sidebar-callout-slot"; import { SidebarWorkspaceList } from "./sidebar-workspace-list"; @@ -457,11 +457,17 @@ function SidebarFooter({ ); } -/** The footer rows in the user's `sidebarFooterItems` order: the Usage item and plugin rows. */ +/** + * The footer rows in the user's `sidebarFooterItems` order: the Usage item and plugin rows. The + * Usage item is left out while it has no summary to show. + */ function SidebarFooterRows({ onBeforeNavigate }: { onBeforeNavigate?: () => void }) { const { items } = useSidebarNavItems("footer"); + const hasUsageSummary = useHasUsageSummary(); const rowsRef = useRef(null); - const visibleItems = items.filter((item) => item.visible); + const visibleItems = items.filter( + (item) => item.visible && (item.kind === "plugin" || hasUsageSummary), + ); if (visibleItems.length === 0) return null; return ( <> diff --git a/packages/app/src/usage/index.ts b/packages/app/src/usage/index.ts index c94fbb01f9..021c627e31 100644 --- a/packages/app/src/usage/index.ts +++ b/packages/app/src/usage/index.ts @@ -1,3 +1,3 @@ export { HostUsageSection } from "./host-usage-section"; export { UsageScreen } from "./usage-screen"; -export { UsageSidebarItem, useOpenUsageScreen } from "./sidebar-item"; +export { UsageSidebarItem, useHasUsageSummary, useOpenUsageScreen } from "./sidebar-item"; diff --git a/packages/app/src/usage/queries.ts b/packages/app/src/usage/queries.ts index 753fb17af5..f5a3fe1ed6 100644 --- a/packages/app/src/usage/queries.ts +++ b/packages/app/src/usage/queries.ts @@ -1,6 +1,6 @@ import { supportsUsageReports } from "@getpaseo/client/internal/daemon-client"; import { useCallback, useMemo } from "react"; -import { useMutation, useQueryClient, type QueryClient } from "@tanstack/react-query"; +import { skipToken, useMutation, useQueryClient, type QueryClient } from "@tanstack/react-query"; import { useShallow } from "zustand/shallow"; import { useFetchQuery } from "@/data/query"; import { @@ -95,6 +95,22 @@ export function useHostUsage(serverId: string): { view: UsageView; refresh: () = return { view, refresh }; } +const NO_REPORTS: UsageReportEntry[] = []; + +/** + * The reports of the sidebar's usage host, which is connected and reports usage; none until they + * load, or without a host. + */ +export function useUsageHostReports(serverId: string | null): UsageReportEntry[] { + const query = useFetchQuery({ + queryKey: usageReportsQueryKey(serverId ?? ""), + queryFn: serverId ? () => listReports(serverId) : skipToken, + dataShape: "list", + staleTimeMs: REPORTS_STALE_TIME_MS, + }); + return query.data ?? NO_REPORTS; +} + /** Every host with whether it is connected and reports usage, in host order. */ export function useUsageHosts(): UsageHost[] { const hosts = useHosts(); diff --git a/packages/app/src/usage/sidebar-item.tsx b/packages/app/src/usage/sidebar-item.tsx index ee8f6f657b..a0bd6a7e15 100644 --- a/packages/app/src/usage/sidebar-item.tsx +++ b/packages/app/src/usage/sidebar-item.tsx @@ -1,5 +1,4 @@ import { router } from "expo-router"; -import { CircleGauge } from "lucide-react-native"; import { Fragment, useCallback, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { @@ -10,7 +9,6 @@ import { type PressableStateCallbackType, } from "react-native"; import { StyleSheet } from "react-native-unistyles"; -import { SidebarHeaderRow } from "@/components/sidebar/sidebar-header-row"; import { SidebarPopoverRoot, SidebarPopoverSurface } from "@/components/sidebar/sidebar-popover"; import { useIsCompactFormFactor } from "@/constants/layout"; import { builtinSidebarNavLabelKey } from "@/sidebar-nav/model"; @@ -19,8 +17,7 @@ import { buildUsageRoute } from "@/utils/host-routes"; import { useHostUsageWithControls } from "./controls"; import { useUsagePreferences, type UsageDisplay } from "./display"; import { useUsageHostId, useUsageHostSelection } from "./hosts"; -import type { UsagePreferences } from "./preferences"; -import { useHostUsage } from "./queries"; +import { useUsageHostReports } from "./queries"; import { UsageSourceIcon } from "./source-icon"; import { UsageMeter } from "./meter"; import { @@ -31,47 +28,31 @@ import { type PinnedUsageLayout, type PinnedUsageSource, } from "./pinned"; -import type { UsageReportEntry } from "./types"; import type { UsageHost } from "./model"; import { UsageBody } from "./usage-section"; -const NO_REPORTS: UsageReportEntry[] = []; -const NO_SOURCES: PinnedUsageSource[] = []; - -/** - * The sidebar footer's usage entry: each summary window's source icon and percent, or a plain - * "Usage" row while no summary window has data. Pressing it opens the Usage screen; on compact - * layouts it opens the usage sheet instead. - */ -export function UsageSidebarItem() { - const { preferences, display } = useUsagePreferences(); - const serverId = useUsageHostId(); - if (!serverId) { - return ; - } - return ( - - ); +/** Each summary window with data on the usage host, under its source; empty while none has. */ +function useUsageSummary(): readonly PinnedUsageSource[] { + const { preferences } = useUsagePreferences(); + const reports = useUsageHostReports(useUsageHostId()); + return useMemo(() => resolvePinnedUsage(reports, preferences), [preferences, reports]); } -function PinnedUsageItem({ - serverId, - preferences, - display, -}: { - serverId: string; - preferences: UsagePreferences; - display: UsageDisplay; -}) { - const { view } = useHostUsage(serverId); - const reports = view.kind === "ready" ? view.reports : NO_REPORTS; - const sources = useMemo(() => resolvePinnedUsage(reports, preferences), [preferences, reports]); - return ; +/** Whether the sidebar Usage item has anything to show. */ +export function useHasUsageSummary(): boolean { + return useUsageSummary().length > 0; +} + +/** + * The sidebar footer's usage entry: each summary window's source icon and percent, and nothing + * while no summary window has data, since the footer's Usage icon already opens the screen. + * Pressing it opens the Usage screen; on compact layouts it opens the usage sheet instead. + */ +export function UsageSidebarItem() { + const { display } = useUsagePreferences(); + const sources = useUsageSummary(); + if (sources.length === 0) return null; + return ; } /** Opens the Usage screen, over the sidebar on compact layouts. */ @@ -85,11 +66,9 @@ export function useOpenUsageScreen(): () => void { } function UsageEntry({ - serverId, sources, display, }: { - serverId: string | null; sources: readonly PinnedUsageSource[]; display: UsageDisplay; }) { @@ -100,30 +79,17 @@ function UsageEntry({ const [open, setOpen] = useState(false); // The sheet mounts on first open; the summary already owns the report query. const [sheetMounted, setSheetMounted] = useState(false); - // Without a host there are no reports to show, so compact goes to the screen, which says so. - const usesSheet = isCompact && serverId !== null; const handlePress = useCallback(() => { - if (!usesSheet) { + if (!isCompact) { openUsageScreen(); return; } setSheetMounted(true); setOpen(true); - }, [openUsageScreen, usesSheet]); + }, [isCompact, openUsageScreen]); - const trigger = - sources.length > 0 ? ( - - ) : ( - - ); - if (!usesSheet) return trigger; + const trigger = ; + if (!isCompact) return trigger; return ( {trigger} diff --git a/packages/desktop/e2e/packaged-app-smoke.js b/packages/desktop/e2e/packaged-app-smoke.js index 385c99fa09..b1b299494a 100644 --- a/packages/desktop/e2e/packaged-app-smoke.js +++ b/packages/desktop/e2e/packaged-app-smoke.js @@ -6,6 +6,8 @@ const path = require("node:path"); const { setTimeout: delay } = require("node:timers/promises"); const { chromium } = require("playwright"); const { extractFile } = require("@electron/asar"); +const { WebSocket } = require("ws"); +const assert = require("node:assert/strict"); const EXECUTABLE_NAME = "Paseo"; const SMOKE_TIMEOUT_MS = 60_000; @@ -31,6 +33,29 @@ function createTempDir(prefix) { return fs.mkdtempSync(path.join(os.tmpdir(), prefix)); } +async function assertBuiltinPluginsStarted(listen) { + const { DaemonClient } = await import("@getpaseo/client/internal/daemon-client"); + const { builtinPlugins } = + await import("../../server/dist/server/server/plugins/builtin/index.js"); + const client = new DaemonClient({ + url: `ws://${listen}/ws`, + clientId: "packaged-builtin-smoke", + webSocketFactory: (url, options) => + new WebSocket(url, options?.protocols, { headers: options?.headers }), + }); + try { + await client.connect(); + const catalog = await client.getPluginCatalog(); + assert.deepEqual( + catalog.map(({ id }) => id).sort(), + [...builtinPlugins].sort(), + "Every built-in plugin must start in the packaged desktop daemon", + ); + } finally { + await client.close(); + } +} + function assertExecutable(filePath, label) { if (!fs.existsSync(filePath)) { throw new Error(`${label} does not exist: ${filePath}`); @@ -915,6 +940,8 @@ async function smokePackagedDesktopApp({ deadline, }); console.log("Packaged desktop smoke: renderer-started desktop daemon reported running"); + await assertBuiltinPluginsStarted(listen); + console.log("Packaged desktop smoke: every built-in plugin started"); await smokeCliShim({ appPath, env }); await smokeCliTerminal({ appPath, env }); if (expectedSandbox !== undefined) { diff --git a/packages/desktop/electron-builder.yml b/packages/desktop/electron-builder.yml index fb01c7b194..6ab5a9b691 100644 --- a/packages/desktop/electron-builder.yml +++ b/packages/desktop/electron-builder.yml @@ -17,6 +17,7 @@ files: - "!node_modules/@getpaseo/**/*.test.*" - "!node_modules/@getpaseo/**/*.spec.*" - "!node_modules/@getpaseo/server/dist/server/web-ui/**" + - "!node_modules/@getpaseo/server/dist/server/builtin-plugins/**" # Workspace packages are symlinked, so anything a dev generates inside them # lands in the asar. CI never has these; a local .debug run adds gigabytes. - "!node_modules/@getpaseo/*/.debug/**" @@ -27,6 +28,9 @@ asarUnpack: - dist/daemon/node-entrypoint-runner.js - node_modules/@getpaseo/server/dist/server/terminal/shell-integration/**/* extraResources: + # Preserve physical compiler inputs, including .d.ts files omitted from packaged node_modules. + - from: ../server/dist/server/builtin-plugins + to: builtin-plugins - from: ../app/dist to: app-dist - from: assets/editor-targets diff --git a/packages/server/src/server/plugins/builtin/index.ts b/packages/server/src/server/plugins/builtin/index.ts index eb6aab147e..c14c027853 100644 --- a/packages/server/src/server/plugins/builtin/index.ts +++ b/packages/server/src/server/plugins/builtin/index.ts @@ -18,6 +18,12 @@ export const builtinPlugins = [ export function resolveBuiltinPluginsRoot(moduleUrl: string | URL = import.meta.url): string { const moduleDir = path.dirname(fileURLToPath(moduleUrl)); + const archiveSegment = `${path.sep}app.asar${path.sep}`; + const archiveIndex = moduleDir.indexOf(archiveSegment); + if (archiveIndex !== -1) { + // esbuild runs outside Electron's filesystem shim and cannot read archive entries. + return path.join(moduleDir.slice(0, archiveIndex), "builtin-plugins"); + } const candidates = [ path.resolve(moduleDir, "..", "..", "..", "builtin-plugins"), path.resolve(moduleDir, "..", "..", "..", "..", "..", "..", "plugins"), diff --git a/packages/server/src/server/plugins/usage-sources.e2e.test.ts b/packages/server/src/server/plugins/usage-sources.e2e.test.ts index c414b20557..df81a8fbb2 100644 --- a/packages/server/src/server/plugins/usage-sources.e2e.test.ts +++ b/packages/server/src/server/plugins/usage-sources.e2e.test.ts @@ -1,8 +1,11 @@ -import { fileURLToPath } from "node:url"; +import { cp, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; import { expect, test } from "vitest"; import { DaemonClient } from "../test-utils/daemon-client.js"; import { createTestPaseoDaemon } from "../test-utils/paseo-daemon.js"; -import { BuiltinPluginLoader } from "./builtin/index.js"; +import { BuiltinPluginLoader, resolveBuiltinPluginsRoot } from "./builtin/index.js"; const fixtureRoot = fileURLToPath(new URL("./test-fixtures/", import.meta.url)); const subprocessDirectory = fileURLToPath( @@ -49,3 +52,219 @@ test("lists built-in and subprocess usage; validates input and isolates fetch er await daemon.close(); } }, 60_000); + +test("a packaged daemon serves usage from external built-in resources", async () => { + const root = await mkdtemp(path.join(os.tmpdir(), "paseo-builtin-asar-")); + const packagePath = path.join("node_modules", "@getpaseo", "server", "dist", "server"); + const resourceRoot = path.join(root, "builtin-plugins"); + // An archive is a file, so the external compiler cannot traverse paths beneath it. + // Actual Electron archive packaging is covered by the packaged-app smoke check. + await writeFile(path.join(root, "app.asar"), "opaque archive fixture"); + const directory = path.join(resourceRoot, "listed"); + await mkdir(path.join(directory, "server"), { recursive: true }); + await writeFile( + path.join(directory, "paseo-plugin.json"), + JSON.stringify({ id: "listed", requirements: { paseo: ">=0.9.2" } }), + ); + await writeFile( + path.join(directory, "server", "contract.d.ts"), + "export interface Account { key: string; label: string; }", + ); + await writeFile( + path.join(directory, "server", "account.ts"), + "import type { Account } from './contract.js'; export const account: Account = { key: 'one', label: 'Test account' };", + ); + await writeFile( + path.join(directory, "index.server.ts"), + `import { z } from 'zod'; +import { account } from './server/account.js'; +export default function contribute(server) { + server.registerUsageSource({ + id: 'fixture', label: 'Fixture', input: z.object({}), + discover: async () => [{}], + identify: async () => account, + fetch: async () => ({ status: 'available', windows: [{ id: 'session', label: 'Session', usedPct: 37 }] }), + }); + return () => {}; +}`, + ); + const moduleUrl = pathToFileURL( + path.join(root, "app.asar", packagePath, "server", "plugins", "builtin", "index.js"), + ); + const daemon = await createTestPaseoDaemon({ + daemonVersion: "0.9.2", + pluginsEnabled: false, + builtinPlugins: new BuiltinPluginLoader(resolveBuiltinPluginsRoot(moduleUrl), ["listed"]), + }); + const client = new DaemonClient({ url: `ws://127.0.0.1:${daemon.port}/ws`, appVersion: "0.9.2" }); + try { + await client.connect(); + const { reports } = await client.listUsageReports(); + expect( + reports.map(({ id, sourceId, sourceLabel, account, report }) => ({ + id, + sourceId, + sourceLabel, + account, + report, + })), + ).toEqual([ + { + id: "fixture:one", + sourceId: "fixture", + sourceLabel: "Fixture", + account: { label: "Test account" }, + report: { + status: "available", + windows: [{ id: "session", label: "Session", usedPct: 37 }], + }, + }, + ]); + } finally { + await client.close(); + await daemon.close(); + await rm(root, { recursive: true, force: true }); + } +}, 60_000); + +test("built-in sources deduplicate cross-harness accounts and fall back without session wiring", async () => { + const root = await mkdtemp(path.join(os.tmpdir(), "usage-cross-harness-")); + const home = path.join(root, "home"); + const plugins = path.join(root, "plugins"); + const codexHome = path.join(home, ".codex"); + const env = { CODEX_HOME: codexHome }; + async function json(file: string, value: unknown) { + await mkdir(path.dirname(file), { recursive: true }); + await writeFile(file, JSON.stringify(value)); + } + async function codexToken(token: string) { + await json(path.join(codexHome, "auth.json"), { + tokens: { access_token: token, account_id: "chatgpt-one" }, + }); + } + await codexToken("fixture-codex"); + const openCodePath = path.join(home, ".local", "share", "opencode", "auth.json"); + await json(openCodePath, { + openai: { type: "oauth", access: "fixture-opencode", accountId: "chatgpt-one" }, + }); + const piPath = path.join(home, ".pi", "agent", "auth.json"); + await json(piPath, { + "openai-codex": { type: "oauth", access: "fixture-pi-codex", accountId: "chatgpt-one" }, + anthropic: { type: "oauth", access: "fixture-pi-claude" }, + }); + await json(path.join(home, ".claude", ".credentials.json"), { + claudeAiOauth: { accessToken: "fixture-claude" }, + }); + const dbPath = path.join(home, ".omp", "agent", "agent.db"); + await mkdir(path.dirname(dbPath), { recursive: true }); + const { DatabaseSync } = await import("node:sqlite"); + const db = new DatabaseSync(dbPath); + db.exec( + "CREATE TABLE auth_credentials (id INTEGER PRIMARY KEY, provider TEXT, credential_type TEXT, data TEXT, disabled_cause TEXT)", + ); + const insert = db.prepare("INSERT INTO auth_credentials VALUES (?, ?, 'oauth', ?, NULL)"); + insert.run( + 1, + "openai-codex", + JSON.stringify({ + access: "fixture-omp-codex", + accountId: "chatgpt-one", + expires: Date.now() + 60_000, + }), + ); + insert.run( + 2, + "anthropic", + JSON.stringify({ access: "fixture-omp-claude", expires: Date.now() + 60_000 }), + ); + db.close(); + for (const source of ["codex", "claude"]) { + const directory = path.join(plugins, `${source}-usage-source`); + await cp(path.join(resolveBuiltinPluginsRoot(), `${source}-usage-source`), directory, { + recursive: true, + }); + const identifyCall = + source === "codex" + ? "identify(inputSchema.parse(input), lookup)" + : "identify(inputSchema.parse(input), fetchApi, Date.now, lookup)"; + // Inject home/env/HTTP at the source boundary; compile and register the real readers. + await writeFile( + path.join(directory, "index.server.ts"), + ` +import { inputSchema } from './shared/input.js'; +import { discover, identify, fetchUsage } from './server/usage.js'; +const lookup = { home: ${JSON.stringify(home)}, env: ${JSON.stringify(env)}, platform: 'linux' }; +async function fetchApi(url, init) { + const token = new Headers(init.headers).get('Authorization'); + if (String(url).endsWith('/profile')) { + const second = token === 'Bearer fixture-pi-claude'; + return new Response(JSON.stringify({ account: { uuid: second ? 'claude-two' : 'claude-one' }, organization: { uuid: second ? 'org-two' : 'org-one' } })); + } + if (token === 'Bearer fixture-revoked') return new Response(null, { status: 401 }); + const used = { 'Bearer fixture-codex': 11, 'Bearer fixture-opencode': 22, 'Bearer fixture-pi-codex': 33, 'Bearer fixture-omp-codex': 44, 'Bearer fixture-omp-claude': 66, 'Bearer fixture-pi-claude': 77 }[token] || 55; + return new Response(JSON.stringify({ rate_limit: { primary_window: { used_percent: used } }, five_hour: { utilization: used } })); +} +export default function contribute(server) { + server.registerUsageSource({ id: '${source}', label: '${source}', input: inputSchema, + discover: () => discover(lookup), + identify: (input) => ${identifyCall}, + fetch: (input) => fetchUsage(inputSchema.parse(input), fetchApi, lookup), + }); + return () => {}; +} +`, + ); + } + const daemon = await createTestPaseoDaemon({ + pluginsEnabled: false, + builtinPlugins: new BuiltinPluginLoader(plugins, ["codex-usage-source", "claude-usage-source"]), + }); + const client = new DaemonClient({ url: `ws://127.0.0.1:${daemon.port}/ws` }); + try { + await client.connect(); + const first = (await client.listUsageReports()).reports; + expect(first.map((entry) => entry.id).sort()).toEqual([ + "claude:claude-one.org-one", + "claude:claude-two.org-two", + "codex:chatgpt-one", + ]); + expect(first.find((entry) => entry.sourceId === "codex")?.report.windows[0]?.usedPct).toBe(11); + expect( + first.find((entry) => entry.id === "claude:claude-two.org-two")?.report.windows[0]?.usedPct, + ).toBe(77); + expect(JSON.stringify(first)).not.toContain("fixture-"); + await json(path.join(home, ".claude", ".credentials.json"), { + claudeAiOauth: { accessToken: "fixture-revoked" }, + }); + const claudeFallback = ( + await client.listUsageReports({ + reportIds: ["claude:claude-one.org-one"], + forceRefresh: true, + }) + ).reports; + expect(claudeFallback[0]?.report.windows[0]?.usedPct).toBe(66); + await codexToken("fixture-revoked"); + const refresh = async () => + (await client.listUsageReports({ reportIds: ["codex:chatgpt-one"], forceRefresh: true })) + .reports[0]; + expect((await refresh())?.report.windows[0]?.usedPct).toBe(22); + await rm(openCodePath); + expect((await refresh())?.report.windows[0]?.usedPct).toBe(33); + await json(piPath, { + "openai-codex": { + type: "oauth", + access: "fixture-pi-codex", + accountId: "chatgpt-one", + expires: 1, + }, + anthropic: { type: "oauth", access: "fixture-pi-claude" }, + }); + expect((await refresh())?.report.windows[0]?.usedPct).toBe(44); + await codexToken("fixture-codex"); + expect((await refresh())?.report.windows[0]?.usedPct).toBe(11); + } finally { + await client.close(); + await daemon.close(); + await rm(root, { recursive: true, force: true }); + } +}, 60_000); diff --git a/packages/server/src/server/plugins/usage-sources/index.test.ts b/packages/server/src/server/plugins/usage-sources/index.test.ts index 41fa0ef2cb..64fd87c566 100644 --- a/packages/server/src/server/plugins/usage-sources/index.test.ts +++ b/packages/server/src/server/plugins/usage-sources/index.test.ts @@ -191,3 +191,89 @@ test("legacy listing distinguishes labeled accounts and preserves unlabeled name (await registry.listLegacyUsage()).providers.map((provider) => provider.displayName), ).toEqual(["claude (work)", "claude (personal)", "claude"]); }); + +test("duplicate logins follow discovery order and fall back on unavailable, error and throws", async () => { + for (const failure of ["unavailable", "error", "throw"]) { + const registry = new UsageSourceRegistry(); + const calls: number[] = []; + registry.register( + source({ + id: "codex", + discover: async () => [0, 1, 2], + identify: async (input) => { + if (input === 0) await new Promise((resolve) => setTimeout(resolve, 10)); + return { key: "same" }; + }, + fetch: async (input) => { + calls.push(Number(input)); + if (input === 0) { + if (failure === "throw") throw new Error("revoked"); + return { status: failure, windows: [] }; + } + return { + status: "available", + windows: [{ id: "session", label: "Session", usedPct: 42 }], + }; + }, + }), + ); + const reports = await registry.listReports(); + expect(reports).toHaveLength(1); + expect(reports[0]?.report.status).toBe("available"); + expect(calls).toEqual([0, 1]); + } +}); + +test("targeted refresh skips a login that changed accounts and preserves fallback", async () => { + let firstAccount = "original"; + const fetched: unknown[] = []; + const registry = new UsageSourceRegistry(); + registry.register( + source({ + id: "source", + discover: async () => [0, 1], + identify: async (input) => ({ key: input === 0 ? firstAccount : "original" }), + fetch: async (input) => { + fetched.push(input); + return { status: "available", windows: [] }; + }, + }), + ); + await registry.listReports(); + firstAccount = "different"; + const reports = await registry.listReports({ + reportIds: ["source:original"], + forceRefresh: true, + }); + expect(reports[0]?.id).toBe("source:original"); + expect(reports[0]?.report.status).toBe("available"); + expect(fetched).toEqual([0, 1]); +}); + +test("discovery identifies a source's logins concurrently and keeps their order", async () => { + const registry = new UsageSourceRegistry(); + const fetched: unknown[] = []; + let release!: () => void; + const allStarted = new Promise((resolve) => (release = resolve)); + let started = 0; + registry.register( + source({ + id: "claude", + discover: async () => [0, 1, 2], + // Each identification waits until all three started; serial identification never finishes. + identify: async (input) => { + if (++started === 3) release(); + await allStarted; + if (input === 0) await new Promise((resolve) => setTimeout(resolve, 10)); + return { key: "same" }; + }, + fetch: async (input) => { + fetched.push(input); + return { status: "unavailable", windows: [] }; + }, + }), + ); + const reports = await registry.listReports(); + expect(reports.map((entry) => entry.id)).toEqual(["claude:same"]); + expect(fetched).toEqual([0, 1, 2]); +}); diff --git a/packages/server/src/server/plugins/usage-sources/index.ts b/packages/server/src/server/plugins/usage-sources/index.ts index ef8b4bd75f..bdc5c344d3 100644 --- a/packages/server/src/server/plugins/usage-sources/index.ts +++ b/packages/server/src/server/plugins/usage-sources/index.ts @@ -15,11 +15,11 @@ export interface UsageSource { interface KnownReport { source: UsageSource; - input: unknown; + inputs: unknown[]; label?: string; } -/** Owns account identity, the latest input for each report, and the five-minute fetch cache. */ +/** Owns account identity, ordered logins for each account, and the five-minute fetch cache. */ export class UsageSourceRegistry { private readonly sources = new Map(); private readonly known = new Map(); @@ -42,7 +42,10 @@ export class UsageSourceRegistry { for (const key of this.cache.keys()) if (key.startsWith(`${id}:`)) this.cache.delete(key); } - private async identify(source: UsageSource, input: unknown): Promise { + private async identify( + source: UsageSource, + input: unknown, + ): Promise<{ id: string; label?: string; error?: true } | null> { try { const account = await source.identify(input); if (account === null) return null; @@ -53,13 +56,11 @@ export class UsageSourceRegistry { throw new Error(`Invalid account identity from ${source.id}`); if (typeof account.key !== "string" || !/^[A-Za-z0-9._-]{1,128}$/.test(account.key)) throw new Error(`Invalid account key from ${source.id}`); - const id = `${source.id}:${account.key}`; - this.known.set(id, { source, input, label: account.label }); - return id; + return { id: `${source.id}:${account.key}`, label: account.label }; } catch (error) { const id = `${source.id}:!error`; this.writeCache(id, this.errorEntry(source, id, error)); - return id; + return { id, error: true }; } } @@ -80,9 +81,28 @@ export class UsageSourceRegistry { try { const inputs = await source.discover(); if (!Array.isArray(inputs)) throw new Error("Usage discovery must return an array"); - return (await Promise.all(inputs.map((input) => this.identify(source, input)))).filter( - (id): id is string => id !== null, - ); + const accounts = await Promise.all(inputs.map((input) => this.identify(source, input))); + // Group in discovery order so an account's logins keep the source's preference. + const knownReports = new Map(); + const ids: string[] = []; + accounts.forEach((account, index) => { + if (account === null) return; + ids.push(account.id); + if (account.error) return; + const known = knownReports.get(account.id); + if (known) known.inputs.push(inputs[index]); + else + knownReports.set(account.id, { + source, + inputs: [inputs[index]], + label: account.label, + }); + }); + for (const key of this.known.keys()) { + if (key.startsWith(`${source.id}:`)) this.known.delete(key); + } + for (const [id, known] of knownReports) this.known.set(id, known); + return ids; } catch (error) { const id = `${source.id}:!error`; this.writeCache(id, this.errorEntry(source, id, error)); @@ -127,20 +147,26 @@ export class UsageSourceRegistry { const pending = this.pending.get(id); if (pending) return pending; const request = (async () => { - let entry: UsageReportEntry; - try { - const report = UsageReportSchema.parse(await known.source.fetch(known.input)); - entry = { - id, - sourceId: known.source.id, - sourceLabel: known.source.label, - icon: known.source.icon, - account: { label: known.label }, - fetchedAt: new Date(this.now()).toISOString(), - report, - }; - } catch (error) { - entry = this.errorEntry(known.source, id, error, known.label); + let entry: UsageReportEntry = { + id, + sourceId: known.source.id, + sourceLabel: known.source.label, + icon: known.source.icon, + account: { label: known.label }, + fetchedAt: new Date(this.now()).toISOString(), + report: { status: "unavailable", windows: [] }, + }; + for (const input of known.inputs) { + try { + // A harness can switch accounts between discovery and a targeted refresh. + const account = await known.source.identify(input); + if (!account || `${known.source.id}:${account.key}` !== id) continue; + const report = UsageReportSchema.parse(await known.source.fetch(input)); + entry.report = report; + if (report.status === "available") break; + } catch (error) { + entry = this.errorEntry(known.source, id, error, known.label); + } } this.writeCache(id, entry); return entry; diff --git a/plugins/claude-usage-source/index.server.ts b/plugins/claude-usage-source/index.server.ts index ce5b16c4f7..fae161db58 100644 --- a/plugins/claude-usage-source/index.server.ts +++ b/plugins/claude-usage-source/index.server.ts @@ -1,6 +1,6 @@ import type { PluginServerContext } from "@getpaseo/plugin/server"; import { inputSchema } from "./shared/input.js"; -import { fetchUsage, identify } from "./server/usage.js"; +import { discover, fetchUsage, identify } from "./server/usage.js"; export default function contribute(server: PluginServerContext) { server.registerUsageSource({ @@ -8,7 +8,7 @@ export default function contribute(server: PluginServerContext) { label: "Claude", icon: "icon.svg", input: inputSchema, - discover: async () => [{}], + discover: () => discover(), identify, fetch: fetchUsage, }); diff --git a/plugins/claude-usage-source/server/keychain.test.ts b/plugins/claude-usage-source/server/keychain.test.ts index 710e3b9cb4..8f5ee0bd99 100644 --- a/plugins/claude-usage-source/server/keychain.test.ts +++ b/plugins/claude-usage-source/server/keychain.test.ts @@ -89,7 +89,7 @@ describe("readClaudeKeychainCredentials", () => { import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { fetchUsage } from "./usage.js"; +import { discover, fetchUsage } from "./usage.js"; function fixtureFetch(expectedToken: string): typeof fetch { return vi.fn(async (_url: RequestInfo | URL, init?: RequestInit) => { @@ -98,41 +98,57 @@ function fixtureFetch(expectedToken: string): typeof fetch { }) as unknown as typeof fetch; } -describe("Claude default credential lookup", () => { - it("reads the configured Claude home file", async () => { +describe("Claude credential routes", () => { + it("reads the explicit Claude credential file", async () => { const dir = mkdtempSync(join(tmpdir(), "claude-usage-input-")); try { writeFileSync( join(dir, ".credentials.json"), JSON.stringify({ claudeAiOauth: { accessToken: "fixture-file" } }), ); - expect((await fetchUsage({}, fixtureFetch("fixture-file"), { claudeHome: dir })).status).toBe( - "available", - ); + expect( + ( + await fetchUsage( + { route: { store: "claude", path: join(dir, ".credentials.json") } }, + fixtureFetch("fixture-file"), + ) + ).status, + ).toBe("available"); } finally { rmSync(dir, { recursive: true, force: true }); } }); - it("default discovery reads the credential file", async () => { + it("discovery reads the configured credential file", async () => { const dir = mkdtempSync(join(tmpdir(), "claude-usage-default-")); try { writeFileSync( join(dir, ".credentials.json"), JSON.stringify({ claudeAiOauth: { accessToken: "fixture-default" } }), ); - vi.stubEnv("CLAUDE_HOME", dir); - expect((await fetchUsage({}, fixtureFetch("fixture-default"))).status).toBe("available"); + const [input] = await discover({ + home: dir, + env: { CLAUDE_CONFIG_DIR: dir }, + platform: "linux", + }); + expect(input).toEqual({ route: { store: "claude", path: join(dir, ".credentials.json") } }); + expect((await fetchUsage(input!, fixtureFetch("fixture-default"))).status).toBe("available"); } finally { rmSync(dir, { recursive: true, force: true }); } }); - it("stays unavailable when the configured home has no credentials", async () => { + it("stays unavailable when the routed file has no credentials", async () => { const dir = mkdtempSync(join(tmpdir(), "claude-empty-config-")); try { const fetchApi = vi.fn() as unknown as typeof fetch; - expect((await fetchUsage({}, fetchApi, { claudeHome: dir, platform: "linux" })).status).toBe( - "unavailable", - ); + expect( + ( + await fetchUsage( + { route: { store: "claude", path: join(dir, ".credentials.json") } }, + fetchApi, + { platform: "linux" }, + ) + ).status, + ).toBe("unavailable"); expect(fetchApi).not.toHaveBeenCalled(); } finally { rmSync(dir, { recursive: true, force: true }); @@ -151,7 +167,7 @@ describe("Claude Keychain token usage", () => { const fetchApi = vi.fn( async () => new Response(null, { status: 401 }), ) as unknown as typeof fetch; - const report = await fetchUsage({}, fetchApi, { + const report = await fetchUsage({ route: { store: "keychain" } }, fetchApi, { platform: "darwin", claudeHome: "/nonexistent-paseo-test-home", readKeychainCredentials: async () => credentials, diff --git a/plugins/claude-usage-source/server/stores.test.ts b/plugins/claude-usage-source/server/stores.test.ts new file mode 100644 index 0000000000..7e0392b88b --- /dev/null +++ b/plugins/claude-usage-source/server/stores.test.ts @@ -0,0 +1,277 @@ +import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, beforeEach, expect, test } from "vitest"; +import { discover, fetchUsage, identify } from "./usage.js"; + +let home: string; +beforeEach(async () => { + home = await mkdtemp(join(tmpdir(), "usage-stores-")); +}); +afterEach(async () => { + await rm(home, { recursive: true, force: true }); +}); +function lookup(env: NodeJS.ProcessEnv = {}) { + return { home, env, platform: "linux" as const, now: () => 1000 }; +} +async function json(path: string, value: unknown) { + const { dirname } = await import("node:path"); + await mkdir(dirname(path), { recursive: true }); + await writeFile(path, JSON.stringify(value)); +} +async function profile(_url: RequestInfo | URL, init?: RequestInit) { + expect(new Headers(init?.headers).get("Authorization")).toMatch(/^Bearer fixture-/); + return new Response( + JSON.stringify({ account: { uuid: "pi-account" }, organization: { uuid: "pi-org" } }), + ); +} +async function account(input: Parameters[0], env: NodeJS.ProcessEnv = {}) { + return identify(input, profile, () => 1000, lookup(env)); +} +async function database(path: string) { + await mkdir((await import("node:path")).dirname(path), { recursive: true }); + const { DatabaseSync } = await import("node:sqlite"); + const db = new DatabaseSync(path); + db.exec( + "CREATE TABLE auth_credentials (id INTEGER PRIMARY KEY, provider TEXT, credential_type TEXT, data TEXT, disabled_cause TEXT)", + ); + const insert = db.prepare("INSERT INTO auth_credentials VALUES (?, ?, ?, ?, ?)"); + insert.run( + 1, + "anthropic", + "oauth", + JSON.stringify({ access: "fixture-omp", accountId: "omp-account", expires: 2000 }), + null, + ); + insert.run( + 2, + "anthropic", + "oauth", + JSON.stringify({ access: "fixture-expired", expires: 500 }), + null, + ); + insert.run( + 3, + "anthropic", + "oauth", + JSON.stringify({ access: "fixture-disabled", expires: 2000 }), + "revoked", + ); + insert.run( + 4, + "anthropic", + "api_key", + JSON.stringify({ access: "fixture-key", expires: 2000 }), + null, + ); + insert.run(5, "anthropic", "oauth", "invalid", null); + db.close(); +} + +test("Pi uses its own OAuth identity and re-reads rotated tokens without writing", async () => { + const path = join(home, ".pi", "agent", "auth.json"); + await json(path, { + anthropic: { type: "oauth", access: "fixture-first", accountId: "pi-account" }, + }); + await json(join(home, ".claude.json"), { + oauthAccount: { accountUuid: "other", organizationUuid: "other-org" }, + }); + const inputs = await discover(lookup()); + expect(inputs).toHaveLength(1); + expect(await account(inputs[0]!)).toEqual({ key: "pi-account.pi-org" }); + await json(path, { + anthropic: { type: "oauth", access: "fixture-rotated", accountId: "pi-account" }, + }); + const before = await readFile(path, "utf8"); + expect( + ( + await fetchUsage( + inputs[0]!, + async (_url, init) => { + expect(new Headers(init?.headers).get("Authorization")).toBe("Bearer fixture-rotated"); + return new Response("{}", { status: 401 }); + }, + lookup(), + ) + ).status, + ).toBe("unavailable"); + expect(await readFile(path, "utf8")).toBe(before); +}); + +test("Pi honors directory override and ignores API keys and malformed files", async () => { + const directory = join(home, "custom-pi"); + const env = { PI_CODING_AGENT_DIR: directory }; + const path = join(directory, "auth.json"); + await json(path, { anthropic: { type: "oauth", access: "fixture-pi", accountId: "pi-account" } }); + expect(await discover(lookup(env))).toEqual([{ route: { store: "pi", path } }]); + await json(path, { anthropic: { type: "api_key", key: "fixture-key" } }); + expect(await discover(lookup(env))).toEqual([]); + await writeFile(path, "broken"); + expect(await discover(lookup(env))).toEqual([]); +}); + +test("expired Pi tokens are unavailable without calling usage or refreshing", async () => { + await json(join(home, ".pi", "agent", "auth.json"), { + anthropic: { type: "oauth", access: "fixture-expired", accountId: "pi-account", expires: 500 }, + }); + const [input] = await discover(lookup()); + expect( + ( + await fetchUsage( + input!, + async () => { + throw new Error("must not fetch expired token"); + }, + lookup(), + ) + ).status, + ).toBe("unavailable"); +}); + +for (const location of ["default", "profile", "xdg", "override", "config"]) { + test(`OMP discovers enabled live OAuth rows in ${location} store read-only`, async () => { + let path = join(home, ".omp", "agent", "agent.db"); + let env: NodeJS.ProcessEnv = { OMP_AUTH_BROKER_URL: "https://broker.test" }; + if (location === "profile") { + env.OMP_PROFILE = "work"; + path = join(home, ".omp", "profiles", "work", "agent", "agent.db"); + } + if (location === "xdg") { + env.XDG_DATA_HOME = join(home, "data"); + env.PI_PROFILE = "work"; + path = join(home, "data", "omp", "profiles", "work", "agent.db"); + } + if (location === "override") { + env.PI_CODING_AGENT_DIR = join(home, "custom"); + path = join(home, "custom", "agent.db"); + } + if (location === "config") { + env.PI_CONFIG_DIR = "custom-config"; + path = join(home, "custom-config", "agent", "agent.db"); + } + await database(path); + const before = await readFile(path); + const inputs = await discover(lookup(env)); + expect(inputs).toEqual([{ route: { store: "omp", path, credentialId: 1 } }]); + expect(await account(inputs[0]!, env)).toEqual({ key: "pi-account.pi-org" }); + expect( + ( + await fetchUsage( + inputs[0]!, + async (_url, init) => { + expect(new Headers(init?.headers).get("Authorization")).toBe("Bearer fixture-omp"); + return new Response("{}", { status: 401 }); + }, + lookup(env), + ) + ).status, + ).toBe("unavailable"); + expect(await readFile(path)).toEqual(before); + }); +} + +test("missing SQLite skips only OMP", async () => { + await database(join(home, ".omp", "agent", "agent.db")); + await json(join(home, ".pi", "agent", "auth.json"), { + anthropic: { type: "oauth", access: "fixture-pi" }, + }); + const inputs = await discover({ ...lookup(), sqlite: () => undefined }); + expect(inputs).toEqual([ + { route: { store: "pi", path: join(home, ".pi", "agent", "auth.json") } }, + ]); +}); + +test("Claude file and keychain are independent discovery candidates", async () => { + const path = join(home, ".claude", ".credentials.json"); + await json(path, { claudeAiOauth: { accessToken: "fixture-file" } }); + const inputs = await discover({ + ...lookup(), + platform: "darwin", + readKeychainCredentials: async () => ({ claudeAiOauth: { accessToken: "fixture-keychain" } }), + }); + expect(inputs).toEqual([{ route: { store: "claude", path } }, { route: { store: "keychain" } }]); +}); + +test("Claude account metadata alone does not discover a login", async () => { + await json(join(home, ".claude.json"), { + oauthAccount: { accountUuid: "old", organizationUuid: "old-org" }, + }); + expect(await discover(lookup())).toEqual([]); + expect( + await identify( + { route: { store: "claude", path: join(home, ".claude", ".credentials.json") } }, + profile, + Date.now, + lookup(), + ), + ).toBeNull(); +}); + +test("keychain identity belongs to its token even when Claude Code metadata names another account", async () => { + await json(join(home, ".claude.json"), { + oauthAccount: { accountUuid: "stale", organizationUuid: "stale-org" }, + }); + const identity = await identify({ route: { store: "keychain" } }, profile, () => 1000, { + ...lookup(), + platform: "darwin", + readKeychainCredentials: async () => ({ + claudeAiOauth: { accessToken: "fixture-keychain-other" }, + }), + }); + expect(identity).toEqual({ key: "pi-account.pi-org" }); +}); + +test("OMP fetch re-reads its row after rotation and skips a newly disabled login", async () => { + const path = join(home, ".omp", "agent", "agent.db"); + await database(path); + const [input] = await discover(lookup()); + const { DatabaseSync } = await import("node:sqlite"); + const db = new DatabaseSync(path); + db.prepare("UPDATE auth_credentials SET data = ? WHERE id = 1").run( + JSON.stringify({ access: "fixture-rotated-omp", accountId: "omp-account", expires: 2000 }), + ); + expect( + ( + await fetchUsage( + input!, + async (_url, init) => { + expect(new Headers(init?.headers).get("Authorization")).toBe( + "Bearer fixture-rotated-omp", + ); + return new Response("{}", { status: 401 }); + }, + lookup(), + ) + ).status, + ).toBe("unavailable"); + db.prepare("UPDATE auth_credentials SET disabled_cause = 'revoked' WHERE id = 1").run(); + db.close(); + expect( + ( + await fetchUsage( + input!, + async () => { + throw new Error("disabled login must not fetch"); + }, + lookup(), + ) + ).status, + ).toBe("unavailable"); +}); + +test("Claude discovery honors CLAUDE_CONFIG_DIR", async () => { + const directory = join(home, "configured-claude"); + const ignoredDirectory = join(home, "other-home"); + await json(join(directory, ".credentials.json"), { + claudeAiOauth: { accessToken: "fixture-configured" }, + }); + await json(join(ignoredDirectory, ".credentials.json"), { + claudeAiOauth: { accessToken: "fixture-other" }, + }); + const inputs = await discover( + lookup({ CLAUDE_CONFIG_DIR: directory, CLAUDE_HOME: ignoredDirectory }), + ); + expect(inputs).toEqual([ + { route: { store: "claude", path: join(directory, ".credentials.json") } }, + ]); +}); diff --git a/plugins/claude-usage-source/server/stores.ts b/plugins/claude-usage-source/server/stores.ts new file mode 100644 index 0000000000..0c41c1478c --- /dev/null +++ b/plugins/claude-usage-source/server/stores.ts @@ -0,0 +1,132 @@ +import { existsSync } from "node:fs"; +import { readFile } from "node:fs/promises"; +import { homedir } from "node:os"; +import { join, resolve } from "node:path"; +import { z } from "zod"; +import { routeSchema } from "../shared/input.js"; + +export interface StoreLookup { + home?: string; + env?: NodeJS.ProcessEnv; + platform?: NodeJS.Platform; + now?: () => number; + sqlite?: () => Sqlite | undefined; +} +interface Database { + prepare(sql: string): { all(...args: string[]): unknown[] }; + close(): void; +} +export interface Sqlite { + DatabaseSync: new (path: string, options: { readOnly: boolean }) => Database; +} +export const oauthSchema = z.object({ + access: z.string().min(1), + accountId: z.string().optional(), + expires: z.number().optional(), +}); +const rowSchema = z.object({ id: z.number().int().positive(), data: z.string() }); +export type Route = z.infer; + +export async function readJson(path: string): Promise { + try { + return JSON.parse(await readFile(path, "utf8")); + } catch { + return null; + } +} + +export function piAuthPath(lookup: StoreLookup): string { + const home = lookup.home ?? homedir(); + const env = lookup.env ?? process.env; + const configured = env.PI_CODING_AGENT_DIR?.trim(); + if (!configured) return join(home, ".pi", "agent", "auth.json"); + if (configured === "~") return join(home, "auth.json"); + const directory = configured.startsWith("~/") + ? join(home, configured.slice(2)) + : resolve(configured); + return join(directory, "auth.json"); +} + +function ompDatabasePath(lookup: StoreLookup): string { + const home = lookup.home ?? homedir(); + const env = lookup.env ?? process.env; + const platform = lookup.platform ?? process.platform; + const profile = (env.OMP_PROFILE ?? env.PI_PROFILE)?.trim() || "default"; + const base = join(home, env.PI_CONFIG_DIR || ".omp"); + const configRoot = profile === "default" ? base : join(base, "profiles", profile); + const defaultAgentDir = join(configRoot, "agent"); + const agentDir = + profile === "default" && env.PI_CODING_AGENT_DIR + ? resolve(env.PI_CODING_AGENT_DIR) + : defaultAgentDir; + if ( + (platform === "linux" || platform === "darwin") && + agentDir === defaultAgentDir && + env.XDG_DATA_HOME + ) { + const xdgBase = join(env.XDG_DATA_HOME, "omp"); + const xdgRoot = profile === "default" ? xdgBase : join(xdgBase, "profiles", profile); + if (existsSync(xdgRoot)) return join(xdgRoot, "agent.db"); + } + return join(agentDir, "agent.db"); +} + +function builtinSqlite(): Sqlite | undefined { + // Node 20 must still compile/load the plugin; only OMP needs this newer builtin. + const parsed = z + .object({ + DatabaseSync: z.custom((value) => typeof value === "function"), + }) + .safeParse(process.getBuiltinModule?.("node:sqlite")); + return parsed.success ? parsed.data : undefined; +} + +function ompRows(path: string, lookup: StoreLookup) { + let db: Database | undefined; + try { + const sqlite = (lookup.sqlite ?? builtinSqlite)(); + if (!sqlite) return []; + db = new sqlite.DatabaseSync(path, { readOnly: true }); + const rows = db + .prepare( + "SELECT id, data FROM auth_credentials WHERE provider = ? AND credential_type = 'oauth' AND disabled_cause IS NULL ORDER BY id", + ) + .all("anthropic"); + return rows.flatMap((value) => { + const row = rowSchema.safeParse(value); + if (!row.success) return []; + try { + const oauth = oauthSchema.parse(JSON.parse(row.data.data)); + if (oauth.expires === undefined || oauth.expires <= (lookup.now ?? Date.now)()) return []; + return [{ id: row.data.id, oauth }]; + } catch { + return []; + } + }); + } catch { + return []; + } finally { + db?.close(); + } +} + +export function discoverOmp(lookup: StoreLookup): Route[] { + const path = ompDatabasePath(lookup); + return ompRows(path, lookup).map(({ id }) => ({ store: "omp", path, credentialId: id })); +} + +export function readOmp(route: Extract, lookup: StoreLookup) { + return ompRows(route.path, lookup).find((row) => row.id === route.credentialId)?.oauth ?? null; +} + +export async function readHarness( + route: Extract, + lookup: StoreLookup = {}, +) { + if (route.store === "omp") return readOmp(route, lookup); + const provider = "anthropic"; + const parsed = z.record(z.string(), z.unknown()).safeParse(await readJson(route.path)); + if (!parsed.success) return null; + const oauth = oauthSchema.extend({ type: z.literal("oauth") }).safeParse(parsed.data[provider]); + return oauth.success ? oauth.data : null; +} diff --git a/plugins/claude-usage-source/server/usage.test.ts b/plugins/claude-usage-source/server/usage.test.ts index b9eb4b6ecf..f282a25ae9 100644 --- a/plugins/claude-usage-source/server/usage.test.ts +++ b/plugins/claude-usage-source/server/usage.test.ts @@ -3,9 +3,13 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { fetchUsage } from "./usage.js"; -import { inputSchema } from "../shared/input.js"; +import { inputSchema, type UsageInput } from "../shared/input.js"; import type { UsageReport } from "@getpaseo/plugin/server/usage"; +function credentialInput(directory: string): UsageInput { + return { route: { store: "claude", path: join(directory, ".credentials.json") } }; +} + function writeClaudeCredentials( dir: string, accessToken: string, @@ -63,7 +67,7 @@ describe("claude usage source", () => { originalEnv = { ...process.env }; process.env["HOME"] = homeDir; process.env["USERPROFILE"] = homeDir; - process.env["CLAUDE_HOME"] = claudeHome; + process.env["CLAUDE_CONFIG_DIR"] = claudeHome; for (const key of [ "APPDATA", "COPILOT_TOKEN", @@ -100,7 +104,9 @@ describe("claude usage source", () => { ) { return { listUsage: async () => { - const report = await fetchUsage({}, (url, init) => fetchApi(url, init)); + const report = await fetchUsage(credentialInput(claudeHome), (url, init) => + fetchApi(url, init), + ); return { providers: [ { @@ -230,9 +236,9 @@ describe("usage bars escalate as they fill", () => { function claudeAt(utilization: number) { writeClaudeCredentials(claudeHome, "at_valid"); - process.env["CLAUDE_HOME"] = claudeHome; + process.env["CLAUDE_CONFIG_DIR"] = claudeHome; return fetchUsage( - {}, + credentialInput(claudeHome), mockFetch( new Map([ [ @@ -286,11 +292,11 @@ describe("Claude usage source scoped weekly limits", () => { function claudeProvider(body: unknown) { writeClaudeCredentials(claudeHome, "at_valid"); - process.env["CLAUDE_HOME"] = claudeHome; + process.env["CLAUDE_CONFIG_DIR"] = claudeHome; const fetchApi = mockFetch( new Map([["https://api.anthropic.com/api/oauth/usage", () => jsonResponse(body)]]), ); - return { provider: { fetchUsage: () => fetchUsage({}, fetchApi) } }; + return { provider: { fetchUsage: () => fetchUsage(credentialInput(claudeHome), fetchApi) } }; } it("renders a scoped weekly limit as its own window", async () => { @@ -467,9 +473,9 @@ describe("Claude usage source scoped limit reconciliation", () => { async function windowsFor(body: Record) { writeClaudeCredentials(claudeHome, "at_valid"); - process.env["CLAUDE_HOME"] = claudeHome; + process.env["CLAUDE_CONFIG_DIR"] = claudeHome; const usage = await fetchUsage( - {}, + credentialInput(claudeHome), mockFetch(new Map([["https://api.anthropic.com/api/oauth/usage", () => jsonResponse(body)]])), ); return usage.windows; @@ -633,7 +639,7 @@ describe("Claude usage source scoped limit reconciliation", () => { }); }); -it("identify reads account and organization from the selected Claude config", async () => { +it("identify uses the credential token profile instead of stale Claude config", async () => { const { mkdtemp, writeFile, rm } = await import("node:fs/promises"); const { identify } = await import("./usage.js"); const directory = await mkdtemp(join(tmpdir(), "claude-identity-")); @@ -648,9 +654,21 @@ it("identify reads account and organization from the selected Claude config", as }, }), ); - expect(await identify({}, undefined, undefined, { accountHome: directory })).toEqual({ - key: "account-uuid.org-uuid", - label: "test@example.com", + writeClaudeCredentials(directory, "fixture-local-account"); + expect( + await identify( + credentialInput(directory), + async () => + jsonResponse({ + account: { uuid: "token-account", email: "token@example.test" }, + organization: { uuid: "token-org" }, + }), + Date.now, + { home: directory, claudeHome: directory }, + ), + ).toEqual({ + key: "token-account.token-org", + label: "token@example.test", }); } finally { await rm(directory, { recursive: true, force: true }); @@ -670,18 +688,23 @@ it("Claude credentials without account metadata use a cached OAuth profile ident { status: 200 }, ); }; - expect(await identify({}, fetchProfile, Date.now, tokenLookup("fixture-claude-token"))).toEqual({ + const lookup = tokenLookup("fixture-claude-token"); + expect( + await identify(credentialInput(lookup.claudeHome), fetchProfile, Date.now, lookup), + ).toEqual({ key: "account-uuid.org-uuid", label: "test@example.com", }); - expect(await identify({}, fetchProfile, Date.now, tokenLookup("fixture-claude-token"))).toEqual({ + expect( + await identify(credentialInput(lookup.claudeHome), fetchProfile, Date.now, lookup), + ).toEqual({ key: "account-uuid.org-uuid", label: "test@example.com", }); expect(calls).toBe(1); }); -it("identifies the same default credentials that fetch uses when oauthAccount is absent", async () => { +it("identifies the same credential route that fetch uses when oauthAccount is absent", async () => { const { identify } = await import("./usage.js"); const directory = mkdtempSync(join(tmpdir(), "claude-credential-identity-")); try { @@ -689,7 +712,7 @@ it("identifies the same default credentials that fetch uses when oauthAccount is writeFileSync(join(directory, ".claude.json"), JSON.stringify({})); let usageRequested = false; await fetchUsage( - {}, + credentialInput(directory), async () => { usageRequested = true; return new Response(null, { status: 401 }); @@ -699,7 +722,7 @@ it("identifies the same default credentials that fetch uses when oauthAccount is expect(usageRequested).toBe(true); let profileRequested = false; const account = await identify( - {}, + credentialInput(directory), async () => { profileRequested = true; return jsonResponse({ @@ -708,7 +731,7 @@ it("identifies the same default credentials that fetch uses when oauthAccount is }); }, Date.now, - { claudeHome: directory, accountHome: directory }, + { claudeHome: directory, home: directory }, ); expect(profileRequested).toBe(true); expect(account).toEqual({ key: "account-uuid.org-uuid", label: "owner@example.test" }); @@ -725,8 +748,8 @@ it("expires cached token profiles after five minutes", async () => { calls++; return jsonResponse({ account: { uuid: "account-uuid" }, organization: { uuid: "org-uuid" } }); }; - const input = {}; const lookup = tokenLookup("ttl-fixture-token"); + const input = credentialInput(lookup.claudeHome); await identify(input, fetchProfile, () => now, lookup); await identify(input, fetchProfile, () => now, lookup); expect(calls).toBe(1); @@ -735,21 +758,21 @@ it("expires cached token profiles after five minutes", async () => { expect(calls).toBe(2); }); -it("identify and fetch share the macOS keychain credential fallback", async () => { +it("identify and fetch share the explicit macOS keychain route", async () => { const { identify } = await import("./usage.js"); const directory = mkdtempSync(join(tmpdir(), "claude-keychain-identity-")); try { const lookup = { platform: "darwin" as const, claudeHome: directory, - accountHome: directory, + home: directory, readKeychainCredentials: async () => ({ claudeAiOauth: { accessToken: "keychain-fixture-token" }, }), }; let usageRequested = false; await fetchUsage( - {}, + { route: { store: "keychain" } }, async () => { usageRequested = true; return new Response(null, { status: 401 }); @@ -758,7 +781,7 @@ it("identify and fetch share the macOS keychain credential fallback", async () = ); expect(usageRequested).toBe(true); const account = await identify( - {}, + { route: { store: "keychain" } }, async () => jsonResponse({ account: { uuid: "keychain-account" }, @@ -773,15 +796,15 @@ it("identify and fetch share the macOS keychain credential fallback", async () = } }); -it("identify and fetch share the default Claude home credentials", async () => { +it("identify and fetch share the explicit Claude credential route", async () => { const { identify } = await import("./usage.js"); const directory = mkdtempSync(join(tmpdir(), "claude-home-identity-")); try { writeClaudeCredentials(directory, "home-fixture-token"); - const lookup = { claudeHome: directory, accountHome: directory }; + const lookup = { claudeHome: directory, home: directory }; let usageRequested = false; await fetchUsage( - {}, + credentialInput(directory), async () => { usageRequested = true; return new Response(null, { status: 401 }); @@ -790,7 +813,7 @@ it("identify and fetch share the default Claude home credentials", async () => { ); expect(usageRequested).toBe(true); const identity = await identify( - {}, + credentialInput(directory), async () => jsonResponse({ account: { uuid: "home-account" }, @@ -813,16 +836,18 @@ it("bounds cached profiles across many token rotations", async () => { return jsonResponse({ account: { uuid: "account" }, organization: { uuid: "org" } }); }; for (let index = 0; index < 129; index++) { - await identify({}, fetchProfile, Date.now, tokenLookup(`rotation-fixture-${index}`)); + const lookup = tokenLookup(`rotation-fixture-${index}`); + await identify(credentialInput(lookup.claudeHome), fetchProfile, Date.now, lookup); } - await identify({}, fetchProfile, Date.now, tokenLookup("rotation-fixture-0")); + const lookup = tokenLookup("rotation-fixture-0"); + await identify(credentialInput(lookup.claudeHome), fetchProfile, Date.now, lookup); expect(calls).toBe(130); }); -it("discovery input rejects explicit credential routes", () => { +it("discovery input requires a route and rejects raw credentials", () => { expect(inputSchema.safeParse({ accessToken: "unused" }).success).toBe(false); expect(inputSchema.safeParse({ configDir: "/unused" }).success).toBe(false); - expect(inputSchema.parse({})).toEqual({}); + expect(() => inputSchema.parse({})).toThrow(); }); const tokenDirectories: string[] = []; @@ -834,5 +859,26 @@ function tokenLookup(accessToken: string) { const directory = mkdtempSync(join(tmpdir(), "claude-profile-token-")); tokenDirectories.push(directory); writeClaudeCredentials(directory, accessToken); - return { claudeHome: directory, accountHome: directory }; + return { claudeHome: directory, home: directory }; } + +it("discovers a Pi OAuth login independently of the CLI login", async () => { + const { discover } = await import("./usage.js"); + const { mkdir, mkdtemp, writeFile, rm } = await import("node:fs/promises"); + const directory = await mkdtemp(join(tmpdir(), "usage-pi-discovery-")); + try { + await mkdir(join(directory, ".pi", "agent"), { recursive: true }); + await writeFile( + join(directory, ".pi", "agent", "auth.json"), + JSON.stringify({ + anthropic: { type: "oauth", access: "fixture-pi", accountId: "pi-account" }, + }), + ); + const inputs = await discover({ home: directory, env: {}, platform: "linux" }); + expect(inputs).toContainEqual({ + route: { store: "pi", path: join(directory, ".pi", "agent", "auth.json") }, + }); + } finally { + await rm(directory, { recursive: true, force: true }); + } +}); diff --git a/plugins/claude-usage-source/server/usage.ts b/plugins/claude-usage-source/server/usage.ts index 70ba4f9273..34464d8b53 100644 --- a/plugins/claude-usage-source/server/usage.ts +++ b/plugins/claude-usage-source/server/usage.ts @@ -1,3 +1,4 @@ +import { discoverOmp, piAuthPath, readHarness, type StoreLookup } from "./stores.js"; import type { UsageInput } from "../shared/input.js"; import { execFile } from "node:child_process"; import { existsSync, promises as fs } from "node:fs"; @@ -75,6 +76,7 @@ type ClaudeLimit = z.infer; const SCOPED_WEEKLY_KIND = "weekly_scoped"; interface ClaudeCredentialRecord { + expires?: number; oauth: { accessToken: string } & NonNullable; } @@ -332,21 +334,21 @@ export async function readClaudeKeychainCredentials( return null; } -interface ClaudeCredentialLookup { - platform?: NodeJS.Platform; +interface ClaudeCredentialLookup extends StoreLookup { readKeychainCredentials?: () => Promise; claudeHome?: string; - accountHome?: string; } -/** Shared credential lookup for usage fetches and account identification. */ -export async function resolveClaudeCredentials( - _input: UsageInput, - lookup: ClaudeCredentialLookup = {}, +function claudeCredentialPath(lookup: ClaudeCredentialLookup): string { + const home = lookup.home ?? homedir(); + const env = lookup.env ?? process.env; + const claudeHome = lookup.claudeHome ?? env.CLAUDE_CONFIG_DIR ?? join(home, ".claude"); + return join(claudeHome, ".credentials.json"); +} + +async function keychainCredentialRecord( + lookup: ClaudeCredentialLookup, ): Promise { - const claudeHome = lookup.claudeHome ?? process.env["CLAUDE_HOME"] ?? join(homedir(), ".claude"); - const fileCredentials = await readCredentialFile(join(claudeHome, ".credentials.json")); - if (fileCredentials) return fileCredentials; if ((lookup.platform ?? process.platform) !== "darwin") return null; const parsed = ClaudeCredentialsSchema.safeParse( await (lookup.readKeychainCredentials ?? readClaudeKeychainCredentials)(), @@ -354,6 +356,34 @@ export async function resolveClaudeCredentials( return parsed.success ? toCredentialRecord(parsed.data) : null; } +export async function discover(lookup: ClaudeCredentialLookup = {}): Promise { + const candidates: UsageInput[] = [ + { route: { store: "claude", path: claudeCredentialPath(lookup) } }, + ]; + if ((lookup.platform ?? process.platform) === "darwin") + candidates.push({ route: { store: "keychain" } }); + candidates.push( + { route: { store: "pi", path: piAuthPath(lookup) } }, + ...discoverOmp(lookup).map((route) => ({ route })), + ); + const present: UsageInput[] = []; + for (const input of candidates) + if (await resolveClaudeCredentials(input, lookup)) present.push(input); + return present; +} + +/** Re-read the selected login; the harness owns token refresh. */ +export async function resolveClaudeCredentials( + input: UsageInput, + lookup: ClaudeCredentialLookup = {}, +): Promise { + const route = input.route; + if (route.store === "claude") return readCredentialFile(route.path); + if (route.store === "keychain") return keychainCredentialRecord(lookup); + const oauth = await readHarness(route, lookup); + return oauth ? { oauth: { accessToken: oauth.access }, expires: oauth.expires } : null; +} + async function readCredentialFile(path: string): Promise { if (!existsSync(path)) return null; try { @@ -418,7 +448,11 @@ export async function fetchUsage( } const credentials = await resolveClaudeCredentials(input, credentialLookup); - if (!credentials) { + if ( + !credentials || + (credentials.expires !== undefined && + credentials.expires <= (credentialLookup.now ?? Date.now)()) + ) { return unavailableUsage(); } @@ -464,11 +498,6 @@ export async function fetchUsage( } // The OAuth usage endpoint meters the active organization selected by the token. -const ClaudeAccountSchema = z.object({ - accountUuid: z.string().min(1), - organizationUuid: z.string().min(1), - emailAddress: z.string().optional(), -}); type ClaudeIdentity = { key: string; label?: string } | null; const PROFILE_TTL_MS = 300_000; const PROFILE_CACHE_LIMIT = 128; @@ -486,19 +515,6 @@ export async function identify( now: () => number = Date.now, credentialLookup: ClaudeCredentialLookup = {}, ) { - const directory = credentialLookup.accountHome ?? homedir(); - try { - const config = z - .object({ oauthAccount: ClaudeAccountSchema }) - .parse(JSON.parse(await fs.readFile(join(directory, ".claude.json"), "utf8"))); - const account = config.oauthAccount; - return { - key: `${account.accountUuid}.${account.organizationUuid}`, - ...(account.emailAddress ? { label: account.emailAddress } : {}), - }; - } catch { - // Credentials may still be present even when account metadata is absent. - } const credentials = await resolveClaudeCredentials(input, credentialLookup); if (!credentials) return null; const token = credentials.oauth.accessToken; diff --git a/plugins/claude-usage-source/shared/input.ts b/plugins/claude-usage-source/shared/input.ts index 9d9a5f432e..76ff6a8795 100644 --- a/plugins/claude-usage-source/shared/input.ts +++ b/plugins/claude-usage-source/shared/input.ts @@ -1,3 +1,15 @@ import { z } from "zod"; -export const inputSchema = z.object({}).strict(); + +export const routeSchema = z.discriminatedUnion("store", [ + z.object({ store: z.enum(["claude", "pi"]), path: z.string().min(1) }).strict(), + z + .object({ + store: z.literal("omp"), + path: z.string().min(1), + credentialId: z.number().int().positive(), + }) + .strict(), + z.object({ store: z.literal("keychain") }).strict(), +]); +export const inputSchema = z.object({ route: routeSchema }).strict(); export type UsageInput = z.infer; diff --git a/plugins/codex-usage-source/index.server.ts b/plugins/codex-usage-source/index.server.ts index 3a7c472784..665860aa5a 100644 --- a/plugins/codex-usage-source/index.server.ts +++ b/plugins/codex-usage-source/index.server.ts @@ -1,6 +1,6 @@ import type { PluginServerContext } from "@getpaseo/plugin/server"; import { inputSchema } from "./shared/input.js"; -import { fetchUsage, identify } from "./server/usage.js"; +import { discover, fetchUsage, identify } from "./server/usage.js"; export default function contribute(server: PluginServerContext) { server.registerUsageSource({ @@ -8,7 +8,7 @@ export default function contribute(server: PluginServerContext) { label: "Codex", icon: "icon.svg", input: inputSchema, - discover: async () => [{}], + discover: () => discover(), identify, fetch: fetchUsage, }); diff --git a/plugins/codex-usage-source/server/stores.test.ts b/plugins/codex-usage-source/server/stores.test.ts new file mode 100644 index 0000000000..191c92d25f --- /dev/null +++ b/plugins/codex-usage-source/server/stores.test.ts @@ -0,0 +1,245 @@ +import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, beforeEach, expect, test } from "vitest"; +import { discover, fetchUsage, identify } from "./usage.js"; + +let home: string; +beforeEach(async () => { + home = await mkdtemp(join(tmpdir(), "usage-stores-")); +}); +afterEach(async () => { + await rm(home, { recursive: true, force: true }); +}); +function lookup(env: NodeJS.ProcessEnv = {}) { + return { home, env, platform: "linux" as const, now: () => 1000 }; +} +async function json(path: string, value: unknown) { + const { dirname } = await import("node:path"); + await mkdir(dirname(path), { recursive: true }); + await writeFile(path, JSON.stringify(value)); +} +async function account(input: Parameters[0], env: NodeJS.ProcessEnv = {}) { + return identify(input, lookup(env)); +} +async function database(path: string) { + await mkdir((await import("node:path")).dirname(path), { recursive: true }); + const { DatabaseSync } = await import("node:sqlite"); + const db = new DatabaseSync(path); + db.exec( + "CREATE TABLE auth_credentials (id INTEGER PRIMARY KEY, provider TEXT, credential_type TEXT, data TEXT, disabled_cause TEXT)", + ); + const insert = db.prepare("INSERT INTO auth_credentials VALUES (?, ?, ?, ?, ?)"); + insert.run( + 1, + "openai-codex", + "oauth", + JSON.stringify({ access: "fixture-omp", accountId: "omp-account", expires: 2000 }), + null, + ); + insert.run( + 2, + "openai-codex", + "oauth", + JSON.stringify({ access: "fixture-expired", expires: 500 }), + null, + ); + insert.run( + 3, + "openai-codex", + "oauth", + JSON.stringify({ access: "fixture-disabled", expires: 2000 }), + "revoked", + ); + insert.run( + 4, + "openai-codex", + "api_key", + JSON.stringify({ access: "fixture-key", expires: 2000 }), + null, + ); + insert.run(5, "openai-codex", "oauth", "invalid", null); + db.close(); +} + +test("Pi uses its own OAuth identity and re-reads rotated tokens without writing", async () => { + const path = join(home, ".pi", "agent", "auth.json"); + await json(path, { + "openai-codex": { type: "oauth", access: "fixture-first", accountId: "pi-account" }, + }); + await json(join(home, ".claude.json"), { + oauthAccount: { accountUuid: "other", organizationUuid: "other-org" }, + }); + const inputs = await discover(lookup()); + expect(inputs).toHaveLength(1); + expect(await account(inputs[0]!)).toEqual({ key: "pi-account" }); + await json(path, { + "openai-codex": { type: "oauth", access: "fixture-rotated", accountId: "pi-account" }, + }); + const before = await readFile(path, "utf8"); + expect( + ( + await fetchUsage( + inputs[0]!, + async (_url, init) => { + expect(new Headers(init?.headers).get("Authorization")).toBe("Bearer fixture-rotated"); + return new Response("{}", { status: 401 }); + }, + lookup(), + ) + ).status, + ).toBe("unavailable"); + expect(await readFile(path, "utf8")).toBe(before); +}); + +test("Pi honors directory override and ignores API keys and malformed files", async () => { + const directory = join(home, "custom-pi"); + const env = { PI_CODING_AGENT_DIR: directory }; + const path = join(directory, "auth.json"); + await json(path, { + "openai-codex": { type: "oauth", access: "fixture-pi", accountId: "pi-account" }, + }); + expect(await discover(lookup(env))).toEqual([{ route: { store: "pi", path } }]); + await json(path, { "openai-codex": { type: "api_key", key: "fixture-key" } }); + expect(await discover(lookup(env))).toEqual([]); + await writeFile(path, "broken"); + expect(await discover(lookup(env))).toEqual([]); +}); + +test("expired Pi tokens are unavailable without calling usage or refreshing", async () => { + await json(join(home, ".pi", "agent", "auth.json"), { + "openai-codex": { + type: "oauth", + access: "fixture-expired", + accountId: "pi-account", + expires: 500, + }, + }); + const [input] = await discover(lookup()); + expect( + ( + await fetchUsage( + input!, + async () => { + throw new Error("must not fetch expired token"); + }, + lookup(), + ) + ).status, + ).toBe("unavailable"); +}); + +for (const location of ["default", "profile", "xdg", "override", "config"]) { + test(`OMP discovers enabled live OAuth rows in ${location} store read-only`, async () => { + let path = join(home, ".omp", "agent", "agent.db"); + let env: NodeJS.ProcessEnv = { OMP_AUTH_BROKER_URL: "https://broker.test" }; + if (location === "profile") { + env.OMP_PROFILE = "work"; + path = join(home, ".omp", "profiles", "work", "agent", "agent.db"); + } + if (location === "xdg") { + env.XDG_DATA_HOME = join(home, "data"); + env.PI_PROFILE = "work"; + path = join(home, "data", "omp", "profiles", "work", "agent.db"); + } + if (location === "override") { + env.PI_CODING_AGENT_DIR = join(home, "custom"); + path = join(home, "custom", "agent.db"); + } + if (location === "config") { + env.PI_CONFIG_DIR = "custom-config"; + path = join(home, "custom-config", "agent", "agent.db"); + } + await database(path); + const before = await readFile(path); + const inputs = await discover(lookup(env)); + expect(inputs).toEqual([{ route: { store: "omp", path, credentialId: 1 } }]); + expect(await account(inputs[0]!, env)).toEqual({ key: "omp-account" }); + expect( + ( + await fetchUsage( + inputs[0]!, + async (_url, init) => { + expect(new Headers(init?.headers).get("Authorization")).toBe("Bearer fixture-omp"); + return new Response("{}", { status: 401 }); + }, + lookup(env), + ) + ).status, + ).toBe("unavailable"); + expect(await readFile(path)).toEqual(before); + }); +} + +test("missing SQLite skips only OMP", async () => { + await database(join(home, ".omp", "agent", "agent.db")); + await json(join(home, ".pi", "agent", "auth.json"), { + "openai-codex": { type: "oauth", access: "fixture-pi" }, + }); + const inputs = await discover({ ...lookup(), sqlite: () => undefined }); + expect(inputs).toEqual([ + { route: { store: "pi", path: join(home, ".pi", "agent", "auth.json") } }, + ]); +}); + +test("Codex and OpenCode discover accounts in preference order with XDG and CODEX_HOME", async () => { + const codex = join(home, "codex"); + const data = join(home, "data"); + const env = { CODEX_HOME: codex, XDG_DATA_HOME: data }; + await json(join(codex, "auth.json"), { + tokens: { access_token: "fixture-codex", account_id: "same" }, + }); + // Codex CLI never reads ~/.config/codex. + await json(join(home, ".config", "codex", "auth.json"), { + tokens: { access_token: "fixture-config", account_id: "same" }, + }); + await json(join(home, ".codex", "auth.json"), { + tokens: { access_token: "fixture-legacy", account_id: "same" }, + }); + await json(join(data, "opencode", "auth.json"), { + openai: { type: "oauth", access: "fixture-open", accountId: "same" }, + }); + const inputs = await discover(lookup(env)); + expect(inputs.map((input) => input.route.store)).toEqual(["codex", "codex", "opencode"]); + for (const input of inputs) expect(await account(input, env)).toEqual({ key: "same" }); + await json(join(data, "opencode", "auth.json"), { openai: { type: "api", key: "fixture-key" } }); + expect(await discover(lookup(env))).toHaveLength(2); +}); + +test("OMP fetch re-reads its row after rotation and skips a newly disabled login", async () => { + const path = join(home, ".omp", "agent", "agent.db"); + await database(path); + const [input] = await discover(lookup()); + const { DatabaseSync } = await import("node:sqlite"); + const db = new DatabaseSync(path); + db.prepare("UPDATE auth_credentials SET data = ? WHERE id = 1").run( + JSON.stringify({ access: "fixture-rotated-omp", accountId: "omp-account", expires: 2000 }), + ); + expect( + ( + await fetchUsage( + input!, + async (_url, init) => { + expect(new Headers(init?.headers).get("Authorization")).toBe( + "Bearer fixture-rotated-omp", + ); + return new Response("{}", { status: 401 }); + }, + lookup(), + ) + ).status, + ).toBe("unavailable"); + db.prepare("UPDATE auth_credentials SET disabled_cause = 'revoked' WHERE id = 1").run(); + db.close(); + expect( + ( + await fetchUsage( + input!, + async () => { + throw new Error("disabled login must not fetch"); + }, + lookup(), + ) + ).status, + ).toBe("unavailable"); +}); diff --git a/plugins/codex-usage-source/server/stores.ts b/plugins/codex-usage-source/server/stores.ts new file mode 100644 index 0000000000..2f20beb24e --- /dev/null +++ b/plugins/codex-usage-source/server/stores.ts @@ -0,0 +1,132 @@ +import { existsSync } from "node:fs"; +import { readFile } from "node:fs/promises"; +import { homedir } from "node:os"; +import { join, resolve } from "node:path"; +import { z } from "zod"; +import { routeSchema } from "../shared/input.js"; + +export interface StoreLookup { + home?: string; + env?: NodeJS.ProcessEnv; + platform?: NodeJS.Platform; + now?: () => number; + sqlite?: () => Sqlite | undefined; +} +interface Database { + prepare(sql: string): { all(...args: string[]): unknown[] }; + close(): void; +} +export interface Sqlite { + DatabaseSync: new (path: string, options: { readOnly: boolean }) => Database; +} +export const oauthSchema = z.object({ + access: z.string().min(1), + accountId: z.string().optional(), + expires: z.number().optional(), +}); +const rowSchema = z.object({ id: z.number().int().positive(), data: z.string() }); +export type Route = z.infer; + +export async function readJson(path: string): Promise { + try { + return JSON.parse(await readFile(path, "utf8")); + } catch { + return null; + } +} + +export function piAuthPath(lookup: StoreLookup): string { + const home = lookup.home ?? homedir(); + const env = lookup.env ?? process.env; + const configured = env.PI_CODING_AGENT_DIR?.trim(); + if (!configured) return join(home, ".pi", "agent", "auth.json"); + if (configured === "~") return join(home, "auth.json"); + const directory = configured.startsWith("~/") + ? join(home, configured.slice(2)) + : resolve(configured); + return join(directory, "auth.json"); +} + +function ompDatabasePath(lookup: StoreLookup): string { + const home = lookup.home ?? homedir(); + const env = lookup.env ?? process.env; + const platform = lookup.platform ?? process.platform; + const profile = (env.OMP_PROFILE ?? env.PI_PROFILE)?.trim() || "default"; + const base = join(home, env.PI_CONFIG_DIR || ".omp"); + const configRoot = profile === "default" ? base : join(base, "profiles", profile); + const defaultAgentDir = join(configRoot, "agent"); + const agentDir = + profile === "default" && env.PI_CODING_AGENT_DIR + ? resolve(env.PI_CODING_AGENT_DIR) + : defaultAgentDir; + if ( + (platform === "linux" || platform === "darwin") && + agentDir === defaultAgentDir && + env.XDG_DATA_HOME + ) { + const xdgBase = join(env.XDG_DATA_HOME, "omp"); + const xdgRoot = profile === "default" ? xdgBase : join(xdgBase, "profiles", profile); + if (existsSync(xdgRoot)) return join(xdgRoot, "agent.db"); + } + return join(agentDir, "agent.db"); +} + +function builtinSqlite(): Sqlite | undefined { + // Node 20 must still compile/load the plugin; only OMP needs this newer builtin. + const parsed = z + .object({ + DatabaseSync: z.custom((value) => typeof value === "function"), + }) + .safeParse(process.getBuiltinModule?.("node:sqlite")); + return parsed.success ? parsed.data : undefined; +} + +function ompRows(path: string, lookup: StoreLookup) { + let db: Database | undefined; + try { + const sqlite = (lookup.sqlite ?? builtinSqlite)(); + if (!sqlite) return []; + db = new sqlite.DatabaseSync(path, { readOnly: true }); + const rows = db + .prepare( + "SELECT id, data FROM auth_credentials WHERE provider = ? AND credential_type = 'oauth' AND disabled_cause IS NULL ORDER BY id", + ) + .all("openai-codex"); + return rows.flatMap((value) => { + const row = rowSchema.safeParse(value); + if (!row.success) return []; + try { + const oauth = oauthSchema.parse(JSON.parse(row.data.data)); + if (oauth.expires === undefined || oauth.expires <= (lookup.now ?? Date.now)()) return []; + return [{ id: row.data.id, oauth }]; + } catch { + return []; + } + }); + } catch { + return []; + } finally { + db?.close(); + } +} + +export function discoverOmp(lookup: StoreLookup): Route[] { + const path = ompDatabasePath(lookup); + return ompRows(path, lookup).map(({ id }) => ({ store: "omp", path, credentialId: id })); +} + +export function readOmp(route: Extract, lookup: StoreLookup) { + return ompRows(route.path, lookup).find((row) => row.id === route.credentialId)?.oauth ?? null; +} + +export async function readHarness( + route: Extract, + lookup: StoreLookup = {}, +) { + if (route.store === "omp") return readOmp(route, lookup); + const provider = route.store === "opencode" ? "openai" : "openai-codex"; + const parsed = z.record(z.string(), z.unknown()).safeParse(await readJson(route.path)); + if (!parsed.success) return null; + const oauth = oauthSchema.extend({ type: z.literal("oauth") }).safeParse(parsed.data[provider]); + return oauth.success ? oauth.data : null; +} diff --git a/plugins/codex-usage-source/server/usage.test.ts b/plugins/codex-usage-source/server/usage.test.ts index e7572f4efe..27aab6e6ac 100644 --- a/plugins/codex-usage-source/server/usage.test.ts +++ b/plugins/codex-usage-source/server/usage.test.ts @@ -1,10 +1,14 @@ -import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, beforeEach, expect, test } from "vitest"; import { fetchUsage, identify } from "./usage.js"; -import { inputSchema } from "../shared/input.js"; +import { inputSchema, type CodexUsageInput } from "../shared/input.js"; + +function authInput(directory: string): CodexUsageInput { + return { route: { store: "codex", path: join(directory, "auth.json") } }; +} let fixtureHome: string; beforeEach(async () => { @@ -42,7 +46,7 @@ function response(headers: HeadersInit, accountId?: string): Promise { ); } -test("default input reads CODEX_HOME auth and preserves the usage request", async () => { +test("explicit route reads Codex auth and preserves the usage request", async () => { const home = await mkdtemp(join(tmpdir(), "usage-codex-")); try { process.env["CODEX_HOME"] = home; @@ -52,7 +56,7 @@ test("default input reads CODEX_HOME auth and preserves the usage request", asyn tokens: { access_token: "fixture-default", account_id: "account-default" }, }), ); - const report = await fetchUsage({}, (_url, init) => + const report = await fetchUsage(authInput(home), (_url, init) => response(init?.headers ?? {}, "account-default"), ); expect(report).toMatchObject({ @@ -65,7 +69,7 @@ test("default input reads CODEX_HOME auth and preserves the usage request", asyn } }); -test("discovery input rejects explicit credential routes", () => { +test("discovery input requires a route and rejects raw credentials", () => { for (const input of [ { codexHome: "/unused" }, { accessToken: "unused" }, @@ -73,18 +77,20 @@ test("discovery input rejects explicit credential routes", () => { ]) { expect(inputSchema.safeParse(input).success).toBe(false); } - expect(inputSchema.parse({})).toEqual({}); + expect(() => inputSchema.parse({})).toThrow(); }); test("identify returns a key when fetch finds Codex token credentials", async () => { await writeAuth("fixture-supplied", "account-supplied"); - await fetchUsage({}, (_url, init) => response(init?.headers ?? {}, "account-supplied")); - expect(await identify({})).toEqual({ key: "account-supplied" }); + await fetchUsage(authInput(fixtureHome), (_url, init) => + response(init?.headers ?? {}, "account-supplied"), + ); + expect(await identify(authInput(fixtureHome))).toEqual({ key: "account-supplied" }); }); test("coerces credit balance and marks a 96 percent window dangerous", async () => { const report = await fetchUsage( - {}, + authInput(fixtureHome), async () => new Response( JSON.stringify({ @@ -110,7 +116,7 @@ test("coerces credit balance and marks a 96 percent window dangerous", async () test("summarizes the session and weekly windows by default, not code review", async () => { const report = await fetchUsage( - {}, + authInput(fixtureHome), async () => new Response( JSON.stringify({ @@ -132,7 +138,7 @@ test("summarizes the session and weekly windows by default, not code review", as test("HTML usage body is unavailable", async () => { const report = await fetchUsage( - {}, + authInput(fixtureHome), async () => new Response("Login", { status: 200 }), ); expect(report.status).toBe("unavailable"); @@ -155,7 +161,7 @@ test("401 leaves auth.json byte for byte unchanged and makes no refresh request" }); await writeFile(authPath, before); let calls = 0; - const report = await fetchUsage({}, async (url) => { + const report = await fetchUsage(authInput(home), async (url) => { expect(String(url)).toBe("https://chatgpt.com/backend-api/wham/usage"); calls++; return new Response(null, { status: 401 }); @@ -168,15 +174,15 @@ test("401 leaves auth.json byte for byte unchanged and makes no refresh request" } }); -test("default auth account claim survives token rotation", async () => { +test("auth account claim survives token rotation", async () => { const token = (suffix: string) => `header.${Buffer.from(JSON.stringify({ "https://api.openai.com/auth": { chatgpt_account_id: "work-id" }, suffix })).toString("base64url")}.signature`; await writeAuth(token("first")); - expect(await identify({})).toEqual({ key: "work-id" }); + expect(await identify(authInput(fixtureHome))).toEqual({ key: "work-id" }); await writeAuth(token("second")); - expect(await identify({})).toEqual({ key: "work-id" }); + expect(await identify(authInput(fixtureHome))).toEqual({ key: "work-id" }); await writeAuth("opaque-token"); - expect(await identify({})).toBeNull(); + expect(await identify(authInput(fixtureHome))).toBeNull(); }); test("identify reads an email label from auth id_token when access token has no profile", async () => { @@ -190,7 +196,7 @@ test("identify reads an email label from auth id_token when access token has no tokens: { account_id: "account-id", access_token: "opaque-token", id_token: idToken }, }), ); - expect(await identify({})).toEqual({ + expect(await identify(authInput(home))).toEqual({ key: "account-id", label: "id-owner@example.test", }); @@ -198,3 +204,23 @@ test("identify reads an email label from auth id_token when access token has no await rm(home, { recursive: true, force: true }); } }); + +test("discovers a Pi OAuth login independently of the CLI login", async () => { + const { discover } = await import("./usage.js"); + const directory = await mkdtemp(join(tmpdir(), "usage-pi-discovery-")); + try { + await mkdir(join(directory, ".pi", "agent"), { recursive: true }); + await writeFile( + join(directory, ".pi", "agent", "auth.json"), + JSON.stringify({ + "openai-codex": { type: "oauth", access: "fixture-pi", accountId: "pi-account" }, + }), + ); + const inputs = await discover({ home: directory, env: {}, platform: "linux" }); + expect(inputs).toContainEqual({ + route: { store: "pi", path: join(directory, ".pi", "agent", "auth.json") }, + }); + } finally { + await rm(directory, { recursive: true, force: true }); + } +}); diff --git a/plugins/codex-usage-source/server/usage.ts b/plugins/codex-usage-source/server/usage.ts index 8620484b6e..1feda550c0 100644 --- a/plugins/codex-usage-source/server/usage.ts +++ b/plugins/codex-usage-source/server/usage.ts @@ -1,4 +1,4 @@ -import { readFile } from "node:fs/promises"; +import { discoverOmp, piAuthPath, readHarness, readJson, type StoreLookup } from "./stores.js"; import { homedir } from "node:os"; import { join } from "node:path"; import { @@ -32,28 +32,56 @@ const responseSchema = z.object({ credits: z.object({ balance: number.optional() }).nullish(), }); -export async function readAuth( - _input: CodexUsageInput, -): Promise<{ token: string; accountId?: string; idToken?: string } | null> { - const candidates = [ - ...(process.env["CODEX_HOME"] ? [join(process.env["CODEX_HOME"], "auth.json")] : []), - join(homedir(), ".config", "codex", "auth.json"), - join(homedir(), ".codex", "auth.json"), +interface Auth { + token: string; + accountId?: string; + idToken?: string; + expires?: number; +} + +export async function discover(lookup: StoreLookup = {}): Promise { + const env = lookup.env ?? process.env; + const home = lookup.home ?? homedir(); + const paths = [ + ...(env.CODEX_HOME ? [join(env.CODEX_HOME, "auth.json")] : []), + join(home, ".codex", "auth.json"), ]; - for (const path of candidates) { - try { - const auth = authSchema.parse(JSON.parse(await readFile(path, "utf8"))); - if (auth.tokens?.access_token) - return { - token: auth.tokens.access_token, - accountId: auth.tokens.account_id, - idToken: auth.tokens.id_token, - }; - } catch { - continue; - } + const candidates: CodexUsageInput[] = [...new Set(paths)].map((path) => ({ + route: { store: "codex", path }, + })); + candidates.push( + { + route: { + store: "opencode", + path: join(env.XDG_DATA_HOME || join(home, ".local", "share"), "opencode", "auth.json"), + }, + }, + { route: { store: "pi", path: piAuthPath(lookup) } }, + ...discoverOmp(lookup).map((route) => ({ route })), + ); + const present: CodexUsageInput[] = []; + for (const input of candidates) if (await readAuth(input, lookup)) present.push(input); + return present; +} + +export async function readAuth( + input: CodexUsageInput, + lookup: StoreLookup = {}, +): Promise { + const route = input.route; + if (route.store !== "codex") { + const oauth = await readHarness(route, lookup); + return oauth + ? { token: oauth.access, accountId: oauth.accountId, expires: oauth.expires } + : null; } - return null; + const auth = authSchema.safeParse(await readJson(route.path)); + if (!auth.success || !auth.data.tokens?.access_token) return null; + return { + token: auth.data.tokens.access_token, + accountId: auth.data.tokens.account_id, + idToken: auth.data.tokens.id_token, + }; } function usageWindow( @@ -73,9 +101,11 @@ function usageWindow( export async function fetchUsage( input: CodexUsageInput, fetchApi: typeof fetch = fetch, + lookup: StoreLookup = {}, ): Promise { - const auth = await readAuth(input); - if (!auth) return { status: "unavailable", windows: [] }; + const auth = await readAuth(input, lookup); + if (!auth || (auth.expires !== undefined && auth.expires <= (lookup.now ?? Date.now)())) + return { status: "unavailable", windows: [] }; const headers: Record = { Authorization: `Bearer ${auth.token}`, Accept: "application/json", @@ -151,8 +181,8 @@ function claimString(value: unknown): string | null { return typeof value === "string" && value.length > 0 ? value : null; } -export async function identify(input: CodexUsageInput) { - const auth = await readAuth(input); +export async function identify(input: CodexUsageInput, lookup: StoreLookup = {}) { + const auth = await readAuth(input, lookup); if (!auth) return null; const access = jwtClaims(auth.token); const id = jwtClaims(auth.idToken); diff --git a/plugins/codex-usage-source/shared/input.ts b/plugins/codex-usage-source/shared/input.ts index baa8ee7ca6..864af1c194 100644 --- a/plugins/codex-usage-source/shared/input.ts +++ b/plugins/codex-usage-source/shared/input.ts @@ -1,4 +1,14 @@ import { z } from "zod"; -export const inputSchema = z.object({}).strict(); +export const routeSchema = z.discriminatedUnion("store", [ + z.object({ store: z.enum(["codex", "opencode", "pi"]), path: z.string().min(1) }).strict(), + z + .object({ + store: z.literal("omp"), + path: z.string().min(1), + credentialId: z.number().int().positive(), + }) + .strict(), +]); +export const inputSchema = z.object({ route: routeSchema }).strict(); export type CodexUsageInput = z.infer; diff --git a/public-docs/plugins/reference.md b/public-docs/plugins/reference.md index d96b18f9d6..80c21f4e8b 100644 --- a/public-docs/plugins/reference.md +++ b/public-docs/plugins/reference.md @@ -336,6 +336,17 @@ export default function contribute(server: PluginServerContext) { `discover()` is required and supplies configured inputs; return `[]` when no account is configured. `identify(input)` returns a stable account key and optional display label without fetching usage, or `null` when there are no credentials. The daemon combines the source ID and key as `:`. The key must be 1–128 characters from `[A-Za-z0-9._-]`, remain stable across token rotation, and identify the account or organization whose quota is metered. Never use a credential or raw email as the key; use `hashAccountKey(value)` when the only stable identity is sensitive. +Return credential-store locators in preference order from `discover()`. The daemon groups inputs with the same source and account key into one card, keeps that order, and tries each login until `fetch()` returns `available`. An unavailable or error report, thrown failure, or missing login falls through to the next input. If none succeeds, the card carries the last attempted report (or `unavailable` if no login remains). Before fetching, the daemon identifies the login again and skips it if its account changed. Re-read credentials in `fetch()` so the harness's token rotations take effect; never refresh or write its tokens. Keep raw credentials out of discovery inputs and reports. + +Built-in subscription sources discover logins independently of agents and provider names: + +| Source | Login preference order | Account key | +| ------ | -------------------------------------------------------- | ----------------------------------------------------- | +| Codex | Codex CLI (`$CODEX_HOME`, `~/.codex`), OpenCode, Pi, OMP | ChatGPT account ID from stored metadata or JWT claims | +| Claude | Claude Code credential file, macOS keychain, Pi, OMP | OAuth account UUID and organization UUID | + +OpenCode honors `XDG_DATA_HOME`; Pi honors `PI_CODING_AGENT_DIR`. OMP honors its directory, profile, and XDG settings, reads enabled unexpired OAuth rows by credential ID, and is skipped when the host lacks `node:sqlite`. Claude Code honors `CLAUDE_CONFIG_DIR`. Each Claude login's OAuth profile supplies its identity. + `usage.list_reports` discovers reports when called without IDs, or reads only the requested known IDs. It caches each report for five minutes and `forceRefresh` refreshes only the returned IDs. Each entry carries `id`, `account.label`, and `fetchedAt`; `fetch()` returns a `UsageReport` with `status` (`available`, `unavailable`, or `error`), optional `planLabel`, and generic `windows`, `balances`, and `details`. The icon is a path to a self-contained SVG under the plugin directory and follows the provider icon restrictions above. ## Entry point and cleanup From a52bdb106cb6bb70bc6d4473b0393ce3a57f61c8 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Thu, 1 Oct 2026 16:22:07 +0200 Subject: [PATCH 049/100] docs(changelog): add 0.11.0-beta.2 release notes --- CHANGELOG.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 77f2ef72e4..f917a4f156 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ # Changelog +## 0.11.0-beta.2 - 2026-10-01 + +### Added + +- Added Usage discovery of ChatGPT logins from Codex CLI, OpenCode, Pi, and OMP ([#5844](https://github.com/getpaseo/paseo/pull/5844)) +- Added Usage discovery of Claude logins from the Claude Code credential file, the macOS keychain, Pi, and OMP ([#5844](https://github.com/getpaseo/paseo/pull/5844)) + +### Changed + +- Changed Usage to show one card per subscription account ([#5844](https://github.com/getpaseo/paseo/pull/5844)) +- Changed Usage to try the account's next login when one is missing, expired, or signed into a different account ([#5844](https://github.com/getpaseo/paseo/pull/5844)) +- Changed the sidebar Usage summary to stay hidden until a usage window has data ([#5844](https://github.com/getpaseo/paseo/pull/5844)) + +### Fixed + +- Fixed built-in plugins, including usage sources, failing to start in packaged desktop apps ([#5844](https://github.com/getpaseo/paseo/pull/5844)) +- Fixed each Paseo skill appearing twice in Codex's skill list ([#5827](https://github.com/getpaseo/paseo/pull/5827) by [@3ae3ae](https://github.com/3ae3ae)) +- Fixed plugin client code failing to import npm packages that declare only `main` or `module` ([#5838](https://github.com/getpaseo/paseo/pull/5838)) + ## 0.11.0-beta.1 - 2026-10-01 ### Added From 8498c741699bfeb062ca6c433f982a6fe063b4bb Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Thu, 1 Oct 2026 16:25:00 +0200 Subject: [PATCH 050/100] chore(release): cut 0.11.0-beta.2 --- package-lock.json | 56 ++++++++++++------------ package.json | 2 +- packages/app/package.json | 2 +- packages/cli/package.json | 8 ++-- packages/client/package.json | 6 +-- packages/desktop/package.json | 2 +- packages/expo-two-way-audio/package.json | 2 +- packages/highlight/package.json | 2 +- packages/plugin/package.json | 10 ++--- packages/protocol/package.json | 2 +- packages/relay/package.json | 2 +- packages/server/package.json | 12 ++--- packages/website/package.json | 2 +- plugins/package.json | 2 +- 14 files changed, 55 insertions(+), 55 deletions(-) diff --git a/package-lock.json b/package-lock.json index e258c05730..e346f1a70c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "paseo", - "version": "0.11.0-beta.1", + "version": "0.11.0-beta.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "paseo", - "version": "0.11.0-beta.1", + "version": "0.11.0-beta.2", "hasInstallScript": true, "license": "Apache-2.0", "workspaces": [ @@ -36415,7 +36415,7 @@ }, "packages/app": { "name": "@getpaseo/app", - "version": "0.11.0-beta.1", + "version": "0.11.0-beta.2", "dependencies": { "@codemirror/commands": "6.10.4", "@codemirror/language": "6.12.4", @@ -37481,12 +37481,12 @@ }, "packages/cli": { "name": "@getpaseo/cli", - "version": "0.11.0-beta.1", + "version": "0.11.0-beta.2", "dependencies": { "@clack/prompts": "^1.0.0", - "@getpaseo/client": "0.11.0-beta.1", - "@getpaseo/protocol": "0.11.0-beta.1", - "@getpaseo/server": "0.11.0-beta.1", + "@getpaseo/client": "0.11.0-beta.2", + "@getpaseo/protocol": "0.11.0-beta.2", + "@getpaseo/server": "0.11.0-beta.2", "chalk": "^5.3.0", "commander": "^12.0.0", "mime-types": "^2.1.35", @@ -37733,10 +37733,10 @@ }, "packages/client": { "name": "@getpaseo/client", - "version": "0.11.0-beta.1", + "version": "0.11.0-beta.2", "dependencies": { - "@getpaseo/protocol": "0.11.0-beta.1", - "@getpaseo/relay": "0.11.0-beta.1", + "@getpaseo/protocol": "0.11.0-beta.2", + "@getpaseo/relay": "0.11.0-beta.2", "zod": "^4.4.3" }, "devDependencies": { @@ -37747,7 +37747,7 @@ }, "packages/desktop": { "name": "@getpaseo/desktop", - "version": "0.11.0-beta.1", + "version": "0.11.0-beta.2", "license": "Apache-2.0", "dependencies": { "@getpaseo/cli": "*", @@ -37991,7 +37991,7 @@ }, "packages/expo-two-way-audio": { "name": "@getpaseo/expo-two-way-audio", - "version": "0.11.0-beta.1", + "version": "0.11.0-beta.2", "license": "MIT", "devDependencies": { "@types/jest": "^29.5.14", @@ -38887,7 +38887,7 @@ }, "packages/highlight": { "name": "@getpaseo/highlight", - "version": "0.11.0-beta.1", + "version": "0.11.0-beta.2", "dependencies": { "@codemirror/language": "6.12.4", "@codemirror/legacy-modes": "^6.5.3", @@ -38916,14 +38916,14 @@ }, "packages/plugin": { "name": "@getpaseo/plugin", - "version": "0.11.0-beta.1", + "version": "0.11.0-beta.2", "dependencies": { "@agentclientprotocol/sdk": "^1.4.0", "use-sync-external-store": "^1.6.0" }, "devDependencies": { - "@getpaseo/client": "0.11.0-beta.1", - "@getpaseo/protocol": "0.11.0-beta.1", + "@getpaseo/client": "0.11.0-beta.2", + "@getpaseo/protocol": "0.11.0-beta.2", "@types/node": "^20.9.0", "@types/react": "~19.2.0", "react": "19.1.0", @@ -38933,8 +38933,8 @@ "zod": "^4.4.3" }, "peerDependencies": { - "@getpaseo/client": "0.11.0-beta.1", - "@getpaseo/protocol": "0.11.0-beta.1", + "@getpaseo/client": "0.11.0-beta.2", + "@getpaseo/protocol": "0.11.0-beta.2", "react": "~19.1.0", "react-native": ">=0.81.5", "zod": "^4.4.3" @@ -38956,7 +38956,7 @@ }, "packages/protocol": { "name": "@getpaseo/protocol", - "version": "0.11.0-beta.1", + "version": "0.11.0-beta.2", "dependencies": { "semver": "^7.8.5", "zod": "^4.4.3" @@ -38983,7 +38983,7 @@ }, "packages/relay": { "name": "@getpaseo/relay", - "version": "0.11.0-beta.1", + "version": "0.11.0-beta.2", "dependencies": { "base64-js": "^1.5.1", "tweetnacl": "^1.0.3", @@ -39201,16 +39201,16 @@ }, "packages/server": { "name": "@getpaseo/server", - "version": "0.11.0-beta.1", + "version": "0.11.0-beta.2", "dependencies": { "@agentclientprotocol/sdk": "^0.17.1", "@anthropic-ai/claude-agent-sdk": "0.3.246", "@anthropic-ai/sdk": "^0.104.2", - "@getpaseo/client": "0.11.0-beta.1", - "@getpaseo/highlight": "0.11.0-beta.1", - "@getpaseo/plugin": "0.11.0-beta.1", - "@getpaseo/protocol": "0.11.0-beta.1", - "@getpaseo/relay": "0.11.0-beta.1", + "@getpaseo/client": "0.11.0-beta.2", + "@getpaseo/highlight": "0.11.0-beta.2", + "@getpaseo/plugin": "0.11.0-beta.2", + "@getpaseo/protocol": "0.11.0-beta.2", + "@getpaseo/relay": "0.11.0-beta.2", "@isaacs/ttlcache": "^2.1.4", "@modelcontextprotocol/sdk": "1.29.0", "@opencode-ai/sdk": "1.14.46", @@ -40131,7 +40131,7 @@ }, "packages/website": { "name": "@getpaseo/website", - "version": "0.11.0-beta.1", + "version": "0.11.0-beta.2", "dependencies": { "@cloudflare/vite-plugin": "^1.29.1", "@cloudflare/workers-types": "^4.20260317.1", @@ -41736,7 +41736,7 @@ }, "plugins": { "name": "@getpaseo/builtin-plugins", - "version": "0.11.0-beta.1", + "version": "0.11.0-beta.2", "dependencies": { "@getpaseo/plugin": "*", "zod": "^4.4.3" diff --git a/package.json b/package.json index fa5a536397..50687c70fb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "paseo", - "version": "0.11.0-beta.1", + "version": "0.11.0-beta.2", "private": true, "description": "Paseo: voice-controlled development environment for local AI coding agents", "keywords": [ diff --git a/packages/app/package.json b/packages/app/package.json index 10dd5f0710..31a314a419 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/app", - "version": "0.11.0-beta.1", + "version": "0.11.0-beta.2", "private": true, "main": "index.ts", "scripts": { diff --git a/packages/cli/package.json b/packages/cli/package.json index 4f9824a90f..a306be2a73 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/cli", - "version": "0.11.0-beta.1", + "version": "0.11.0-beta.2", "description": "Paseo CLI - control your AI coding agents from the command line", "bin": { "paseo": "bin/paseo" @@ -28,9 +28,9 @@ }, "dependencies": { "@clack/prompts": "^1.0.0", - "@getpaseo/client": "0.11.0-beta.1", - "@getpaseo/protocol": "0.11.0-beta.1", - "@getpaseo/server": "0.11.0-beta.1", + "@getpaseo/client": "0.11.0-beta.2", + "@getpaseo/protocol": "0.11.0-beta.2", + "@getpaseo/server": "0.11.0-beta.2", "chalk": "^5.3.0", "commander": "^12.0.0", "mime-types": "^2.1.35", diff --git a/packages/client/package.json b/packages/client/package.json index 2d092bb2e1..3adf29e33f 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/client", - "version": "0.11.0-beta.1", + "version": "0.11.0-beta.2", "description": "Paseo client SDK package", "files": [ "dist", @@ -39,8 +39,8 @@ "test": "vitest run" }, "dependencies": { - "@getpaseo/protocol": "0.11.0-beta.1", - "@getpaseo/relay": "0.11.0-beta.1", + "@getpaseo/protocol": "0.11.0-beta.2", + "@getpaseo/relay": "0.11.0-beta.2", "zod": "^4.4.3" }, "devDependencies": { diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 265476e21c..74b481450f 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/desktop", - "version": "0.11.0-beta.1", + "version": "0.11.0-beta.2", "private": true, "description": "Paseo desktop app (Electron wrapper)", "homepage": "https://paseo.sh", diff --git a/packages/expo-two-way-audio/package.json b/packages/expo-two-way-audio/package.json index 3b3434c510..dae98ae5a4 100644 --- a/packages/expo-two-way-audio/package.json +++ b/packages/expo-two-way-audio/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/expo-two-way-audio", - "version": "0.11.0-beta.1", + "version": "0.11.0-beta.2", "description": "Native module for two way audio streaming", "keywords": [ "ExpoTwoWayAudio", diff --git a/packages/highlight/package.json b/packages/highlight/package.json index 5fd1c63f82..8efa5ba85b 100644 --- a/packages/highlight/package.json +++ b/packages/highlight/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/highlight", - "version": "0.11.0-beta.1", + "version": "0.11.0-beta.2", "files": [ "dist", "!dist/**/*.map", diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 74b6e7a91d..6cf9c32605 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/plugin", - "version": "0.11.0-beta.1", + "version": "0.11.0-beta.2", "description": "Paseo plugin SDK package", "files": [ "dist", @@ -70,8 +70,8 @@ "use-sync-external-store": "^1.6.0" }, "devDependencies": { - "@getpaseo/client": "0.11.0-beta.1", - "@getpaseo/protocol": "0.11.0-beta.1", + "@getpaseo/client": "0.11.0-beta.2", + "@getpaseo/protocol": "0.11.0-beta.2", "@types/node": "^20.9.0", "@types/react": "~19.2.0", "react": "19.1.0", @@ -81,8 +81,8 @@ "zod": "^4.4.3" }, "peerDependencies": { - "@getpaseo/client": "0.11.0-beta.1", - "@getpaseo/protocol": "0.11.0-beta.1", + "@getpaseo/client": "0.11.0-beta.2", + "@getpaseo/protocol": "0.11.0-beta.2", "react": "~19.1.0", "react-native": ">=0.81.5", "zod": "^4.4.3" diff --git a/packages/protocol/package.json b/packages/protocol/package.json index 8e9e6f3e21..ea5a112cb0 100644 --- a/packages/protocol/package.json +++ b/packages/protocol/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/protocol", - "version": "0.11.0-beta.1", + "version": "0.11.0-beta.2", "description": "Paseo shared protocol schemas and wire types", "files": [ "dist", diff --git a/packages/relay/package.json b/packages/relay/package.json index 5789910648..8b6e7d71a9 100644 --- a/packages/relay/package.json +++ b/packages/relay/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/relay", - "version": "0.11.0-beta.1", + "version": "0.11.0-beta.2", "description": "Paseo relay for bridging daemon and client connections", "files": [ "dist", diff --git a/packages/server/package.json b/packages/server/package.json index a2c1506707..1de8aa483b 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/server", - "version": "0.11.0-beta.1", + "version": "0.11.0-beta.2", "description": "Paseo backend server", "files": [ "dist/server", @@ -107,11 +107,11 @@ "@agentclientprotocol/sdk": "^0.17.1", "@anthropic-ai/claude-agent-sdk": "0.3.246", "@anthropic-ai/sdk": "^0.104.2", - "@getpaseo/client": "0.11.0-beta.1", - "@getpaseo/highlight": "0.11.0-beta.1", - "@getpaseo/plugin": "0.11.0-beta.1", - "@getpaseo/protocol": "0.11.0-beta.1", - "@getpaseo/relay": "0.11.0-beta.1", + "@getpaseo/client": "0.11.0-beta.2", + "@getpaseo/highlight": "0.11.0-beta.2", + "@getpaseo/plugin": "0.11.0-beta.2", + "@getpaseo/protocol": "0.11.0-beta.2", + "@getpaseo/relay": "0.11.0-beta.2", "@isaacs/ttlcache": "^2.1.4", "@modelcontextprotocol/sdk": "1.29.0", "@opencode-ai/sdk": "1.14.46", diff --git a/packages/website/package.json b/packages/website/package.json index f59e082539..e6f702b3ea 100644 --- a/packages/website/package.json +++ b/packages/website/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/website", - "version": "0.11.0-beta.1", + "version": "0.11.0-beta.2", "private": true, "type": "module", "scripts": { diff --git a/plugins/package.json b/plugins/package.json index 8182ce1bda..e9f3cb1ed6 100644 --- a/plugins/package.json +++ b/plugins/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/builtin-plugins", - "version": "0.11.0-beta.1", + "version": "0.11.0-beta.2", "private": true, "type": "module", "scripts": { From b5b43edd65cc1253493b13cca3941dd390df6ef3 Mon Sep 17 00:00:00 2001 From: "paseo-ai[bot]" <266920839+paseo-ai[bot]@users.noreply.github.com> Date: Thu, 1 Oct 2026 14:35:44 +0000 Subject: [PATCH 051/100] fix: update lockfile signatures and Nix hash [skip ci] --- nix/npm-deps.hash | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nix/npm-deps.hash b/nix/npm-deps.hash index 906f4c5f7f..9ef4e45e82 100644 --- a/nix/npm-deps.hash +++ b/nix/npm-deps.hash @@ -1 +1 @@ -sha256-wS6J8HSLqxaFmdX849bowzUkTnPueRM61rcdcvFEUuQ= +sha256-UZ9fPdCIJzghuxPXK9Kzt5fJmjyVwfaWsGiwXAh/zZI= From 05e94ab52ebf20ee6012c2bafbd892856643a7e5 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Fri, 2 Oct 2026 00:30:11 +0100 Subject: [PATCH 052/100] Keep usage logins visible and explain unavailable quota (#5876) * fix(usage): keep existing logins visible and explain unavailable quota Credential discovery must preserve accounts whose login exists even when it has expired. Keep vendor failures on the discovered account and isolate discovery failures without inventing provider cards. * test(usage): align credential route tests with account discovery --- docs/providers.md | 11 +- packages/app/e2e/browser/usage-screen.spec.ts | 116 ++++++++++- .../app/e2e/browser/usage-settings.spec.ts | 17 +- .../e2e/browser/usage-sidebar-item.spec.ts | 5 +- .../app/e2e/support/helpers/usage-login.ts | 66 ++++++ .../app/e2e/support/helpers/usage-reports.ts | 9 +- .../e2e/support/helpers/usage-sidebar-item.ts | 3 +- packages/app/src/usage/card.tsx | 28 ++- packages/app/src/usage/copy.test.ts | 18 ++ packages/app/src/usage/copy.ts | 12 ++ packages/app/src/usage/pinned.test.ts | 15 ++ packages/app/src/usage/pinned.ts | 1 + packages/app/src/usage/types.ts | 5 +- packages/app/src/usage/window-bar.tsx | 73 +++++-- packages/client/src/daemon-client.test.ts | 167 +++++---------- packages/client/src/daemon-client.ts | 48 +++-- packages/plugin/src/server/usage.ts | 44 ++-- packages/protocol/src/messages.ts | 33 ++- packages/server/src/server/plugins/index.ts | 9 +- .../server/plugins/plugin-process-protocol.ts | 9 - .../src/server/plugins/plugin-process.ts | 18 +- .../src/server/plugins/runtime.posix.test.ts | 1 - packages/server/src/server/plugins/runtime.ts | 11 - .../usage-source-directory/index.server.ts | 13 +- .../usage-source/index.server.ts | 13 +- .../server/plugins/usage-sources.e2e.test.ts | 79 +++++-- .../plugins/usage-sources/index.test.ts | 192 +++++++++--------- .../src/server/plugins/usage-sources/index.ts | 168 +++++++-------- plugins/claude-usage-source/index.server.ts | 3 +- .../server/keychain.test.ts | 47 +++-- .../claude-usage-source/server/stores.test.ts | 108 +++++----- plugins/claude-usage-source/server/stores.ts | 1 - .../claude-usage-source/server/usage.test.ts | 185 +++++++++++++---- plugins/claude-usage-source/server/usage.ts | 108 ++++++---- plugins/codex-usage-source/index.server.ts | 3 +- .../codex-usage-source/server/stores.test.ts | 70 ++++--- plugins/codex-usage-source/server/stores.ts | 1 - .../codex-usage-source/server/usage.test.ts | 68 +++++-- plugins/codex-usage-source/server/usage.ts | 34 ++-- plugins/copilot-usage-source/index.server.ts | 5 +- .../copilot-usage-source/server/usage.test.ts | 57 +++++- plugins/copilot-usage-source/server/usage.ts | 67 +++--- plugins/copilot-usage-source/shared/input.ts | 8 +- plugins/cursor-usage-source/index.server.ts | 5 +- .../cursor-usage-source/server/usage.test.ts | 68 +++++-- plugins/cursor-usage-source/server/usage.ts | 107 +++++----- plugins/cursor-usage-source/shared/input.ts | 8 +- plugins/grok-usage-source/index.server.ts | 5 +- .../grok-usage-source/server/usage.test.ts | 57 +++++- plugins/grok-usage-source/server/usage.ts | 50 +++-- plugins/grok-usage-source/shared/input.ts | 8 +- plugins/kimi-usage-source/index.server.ts | 5 +- .../kimi-usage-source/server/usage.test.ts | 65 +++++- plugins/kimi-usage-source/server/usage.ts | 111 ++++------ plugins/kimi-usage-source/shared/input.ts | 8 +- plugins/minimax-usage-source/index.server.ts | 5 +- .../minimax-usage-source/server/usage.test.ts | 78 +++++-- plugins/minimax-usage-source/server/usage.ts | 164 ++++++--------- plugins/minimax-usage-source/shared/input.ts | 8 +- plugins/muse-provider/server/usage.ts | 18 +- plugins/muse-provider/test/provider.test.ts | 24 ++- .../opencode-go-usage-source/index.server.ts | 3 +- .../server/usage.test.ts | 52 ++--- .../opencode-go-usage-source/server/usage.ts | 20 +- .../opencode-go-usage-source/shared/input.ts | 3 +- plugins/zai-usage-source/index.server.ts | 5 +- plugins/zai-usage-source/server/usage.test.ts | 57 +++++- plugins/zai-usage-source/server/usage.ts | 24 ++- plugins/zai-usage-source/shared/input.ts | 8 +- public-docs/plugins/reference.md | 114 +++++++++-- 70 files changed, 1919 insertions(+), 1110 deletions(-) create mode 100644 packages/app/e2e/support/helpers/usage-login.ts create mode 100644 packages/app/src/usage/copy.test.ts diff --git a/docs/providers.md b/docs/providers.md index 0036c2e6f2..9aa2fe8bf6 100644 --- a/docs/providers.md +++ b/docs/providers.md @@ -187,15 +187,8 @@ promise for completion: equal results, including equal discovery timestamps, emi ## Usage sources -Usage is fetched on demand from plugin usage sources. Each source registers through `server.registerUsageSource()` with an input schema, `fetch(input)`, and required `discover()`. The daemon discovers configured accounts, validates inputs in the plugin runtime, caches each source/input result for five minutes, and returns `usage.list_reports.response`. A source report has an account key, availability status, plan label, windows, balances, and details. - -Create a built-in source under `plugins/-usage-source/` with the same manifest, entry, `server/`, `shared/`, and `icon.svg` layout as an external plugin. Add its ID to `builtinPlugins` in `packages/server/src/server/plugins/builtin/index.ts`. Keep credential discovery, API parsing, and normalization inside the source; use helpers from `@getpaseo/plugin/server/usage`. The wire shape remains source agnostic. See [plugin usage sources](plugins.md#usage-sources). - -`provider.usage.list` remains a compatibility RPC for older apps. It maps discovered reports to `ProviderUsage`. New clients use `usage.list_reports` after checking `server_info.features.usageSources`. - -### Credentials are read only - -A source reads provider credentials without writing them. On 401 or 403 it returns `unavailable` and leaves refresh to the provider CLI. Redeeming a refresh token here would invalidate the CLI's copy; rewriting a parsed credential file could drop fields the source does not model. +See the [public usage source reference](../public-docs/plugins/reference.md#usage-sources) for the +contract, account identity, login fallback, and read-only credential rules. --- diff --git a/packages/app/e2e/browser/usage-screen.spec.ts b/packages/app/e2e/browser/usage-screen.spec.ts index 6512a8bd8f..71f3dfd8cc 100644 --- a/packages/app/e2e/browser/usage-screen.spec.ts +++ b/packages/app/e2e/browser/usage-screen.spec.ts @@ -1,3 +1,4 @@ +import { tmpdir } from "node:os"; import { randomUUID } from "node:crypto"; import path from "node:path"; import type { Page } from "@playwright/test"; @@ -12,7 +13,38 @@ import { type UsageListRequest, type UsageReportsFixture, } from "../support/helpers/usage-reports"; -import { expectPinnedUsage, usageItem } from "../support/helpers/usage-sidebar-item"; +import { + installLoginUsage, + openUsage, + refreshLoginUsage, + hoverUsageWindow, +} from "../support/helpers/usage-login"; +import { expectPinnedUsage, togglePin, usageItem } from "../support/helpers/usage-sidebar-item"; + +const emptyHome = path.join(tmpdir(), `paseo-usage-empty-${randomUUID()}`); +test.use({ + e2eDaemonEnvironment: { + HOME: emptyHome, + USERPROFILE: emptyHome, + CODEX_HOME: emptyHome, + CLAUDE_CONFIG_DIR: emptyHome, + XDG_DATA_HOME: emptyHome, + PI_CODING_AGENT_DIR: emptyHome, + COPILOT_TOKEN: "", + GITHUB_TOKEN: "", + GITHUB_PAT: "", + CURSOR_ACCESS_TOKEN: "", + CURSOR_TOKEN: "", + GROK_API_KEY: "", + GROK_TOKEN: "", + KIMI_TOKEN: "", + KIMI_API_KEY: "", + KIMI_CODE_HOME: emptyHome, + MINIMAX_API_KEY: "", + ZAI_API_KEY: "", + GLM_API_KEY: "", + }, +}); function forcedRefreshes(usage: UsageReportsFixture): UsageListRequest[] { return usage.listRequests().filter((request) => request.forceRefresh); @@ -73,7 +105,10 @@ test.describe("usage screen", () => { fetchedAt: "2026-01-01T00:00:00.000Z", sourceId: "beta", sourceLabel: "Beta plan", - report: { status: "unavailable", windows: [] }, + report: { + status: "unavailable", + problem: { kind: "no_quota", detail: "No active coding plan" }, + }, }, ], ], @@ -244,3 +279,80 @@ test.describe("usage screen", () => { } }); }); + +const loginWindows: UsageReportEntry["report"] = { + status: "available", + windows: [ + { id: "five_hour", label: "Session", shortLabel: "5h", summary: true, usedPct: 31 }, + { id: "weekly", label: "Weekly", shortLabel: "wk", summary: true, usedPct: 54 }, + ], +}; + +test("expired login refreshes to windows with visible pin toggles", async ({ page }) => { + const fixture = await installLoginUsage({ + status: "unavailable", + problem: { + kind: "expired", + expiresAt: new Date(Date.now() - 3_600_000).toISOString(), + refreshedBy: "claude", + }, + }); + try { + await gotoAppShell(page); + await openUsage(page); + await expect( + page.getByText("Login expired 1h ago. Run claude to refresh it.", { exact: true }), + ).toBeVisible(); + await expect(page.getByText("Unavailable", { exact: true })).toBeVisible(); + await qaScreenshot(page, "usage-expired-login"); + await fixture.setReport(loginWindows); + await refreshLoginUsage(page); + await expect( + page.getByText("Login expired 1h ago. Run claude to refresh it.", { exact: true }), + ).toHaveCount(0); + await expectPinnedUsage(page, ["31% 5h", "54% wk"]); + const row = page.getByRole("checkbox", { name: /^Pin Claude Weekly, / }); + await expect(row.getByTestId("usage-pin-glyph-unpinned")).toHaveCSS("opacity", "0"); + await hoverUsageWindow(page, "Weekly"); + await expect(page.getByText("Pin", { exact: true })).toBeVisible(); + await expect(row.getByTestId("usage-pin-glyph-unpinned")).toHaveCSS("opacity", "1"); + await expect(row.getByTestId("usage-pin-glyph-unpinned").locator("svg")).toHaveAttribute( + "fill", + "none", + ); + await qaScreenshot(page, "usage-pin-hover"); + await togglePin(page.getByTestId("usage-report-login-journey:account"), "Claude", "Weekly"); + await expect(row.getByTestId("usage-pin-glyph-pinned")).toHaveCSS("opacity", "1"); + await expect(row.getByTestId("usage-pin-glyph-pinned").locator("svg")).not.toHaveAttribute( + "fill", + "none", + ); + await expectPinnedUsage(page, ["54% wk"]); + await qaScreenshot(page, "usage-pin-selected"); + await togglePin(page.getByTestId("usage-report-login-journey:account"), "Claude", "Weekly"); + await expect(row.getByTestId("usage-pin-glyph-unpinned")).toHaveCSS("opacity", "1"); + await expect(row.getByTestId("usage-pin-glyph-unpinned").locator("svg")).toHaveAttribute( + "fill", + "none", + ); + await expectPinnedUsage(page, ["31% 5h", "54% wk"]); + } finally { + await fixture.cleanup(); + } +}); + +test("compact usage rows always show the pin glyph", async ({ page }) => { + const fixture = await installLoginUsage(loginWindows); + try { + await page.setViewportSize({ width: 390, height: 844 }); + await gotoAppShell(page); + await openUsage(page); + const card = page.getByTestId("usage-report-login-journey:account"); + await expect(card.getByTestId("usage-pin-glyph-unpinned")).toHaveCount(2); + await expect(card.getByTestId("usage-pin-glyph-unpinned").nth(0)).toHaveCSS("opacity", "1"); + await expect(card.getByTestId("usage-pin-glyph-unpinned").nth(1)).toHaveCSS("opacity", "1"); + await qaScreenshot(page, "usage-pin-compact"); + } finally { + await fixture.cleanup(); + } +}); diff --git a/packages/app/e2e/browser/usage-settings.spec.ts b/packages/app/e2e/browser/usage-settings.spec.ts index 5ad817aeb1..617bd121b5 100644 --- a/packages/app/e2e/browser/usage-settings.spec.ts +++ b/packages/app/e2e/browser/usage-settings.spec.ts @@ -18,7 +18,9 @@ function forcedRefreshCount(usage: UsageReportsFixture): number { function report(input: { sourceId: string; sourceLabel: string; - report: Partial; + report: + | Partial> + | Exclude; }): UsageReportEntry { return { id: `${input.sourceId}:account`, @@ -27,11 +29,14 @@ function report(input: { sourceId: input.sourceId, sourceLabel: input.sourceLabel, icon: ICON, - report: { - status: "available", - windows: [], - ...input.report, - }, + report: + input.report.status === "error" || input.report.status === "unavailable" + ? input.report + : { + status: "available", + windows: [], + ...input.report, + }, }; } diff --git a/packages/app/e2e/browser/usage-sidebar-item.spec.ts b/packages/app/e2e/browser/usage-sidebar-item.spec.ts index 0b1f04fed7..8a1d4bb7f0 100644 --- a/packages/app/e2e/browser/usage-sidebar-item.spec.ts +++ b/packages/app/e2e/browser/usage-sidebar-item.spec.ts @@ -262,7 +262,10 @@ test("without summary data the footer drops the Usage item and keeps the Usage i fetchedAt: new Date().toISOString(), sourceId: "alpha", sourceLabel: "Alpha plan", - report: { status: "unavailable", windows: [] }, + report: { + status: "unavailable", + problem: { kind: "no_quota", detail: "No active coding plan" }, + }, }, ], ], diff --git a/packages/app/e2e/support/helpers/usage-login.ts b/packages/app/e2e/support/helpers/usage-login.ts new file mode 100644 index 0000000000..5a2bef072d --- /dev/null +++ b/packages/app/e2e/support/helpers/usage-login.ts @@ -0,0 +1,66 @@ +import { mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import type { UsageReport } from "@getpaseo/protocol/messages"; +import type { Page } from "@playwright/test"; +import { connectNewWorkspaceDaemonClient } from "./new-workspace"; +import { pluginRequirements } from "./plugin-fixture"; + +/** A real store-backed plugin on the worker's isolated daemon. */ +export async function installLoginUsage(initialReport: UsageReport) { + const directory = await mkdtemp(path.join(tmpdir(), "usage-login-journey-")); + const client = await connectNewWorkspaceDaemonClient({ ownProjects: false }); + const previous = await client.getDaemonConfig(); + const store = path.join(directory, "report.json"); + const setReport = (report: UsageReport) => writeFile(store, JSON.stringify(report)); + const cleanup = async () => { + try { + await client.removePlugin("login-journey"); + await client.patchDaemonConfig({ pluginsEnabled: previous.config.pluginsEnabled ?? false }); + } finally { + await client.close(); + await rm(directory, { recursive: true, force: true }); + } + }; + try { + await setReport(initialReport); + await writeFile( + path.join(directory, "paseo-plugin.json"), + JSON.stringify({ id: "login-journey", requirements: pluginRequirements }), + ); + await writeFile( + path.join(directory, "index.server.ts"), + ` +import {readFile} from "node:fs/promises"; +import {z} from "zod"; +export default function contribute(server) { + server.registerUsageSource({id: "login-journey", label: "Claude", input: z.object({}), + discover: async () => [{key: "account", input: {}}], + fetch: async () => JSON.parse(await readFile(${JSON.stringify(store)}, "utf8")), + }); + return () => {}; +}`, + ); + await client.patchDaemonConfig({ pluginsEnabled: true }); + await client.installPluginSource({ source: directory }); + return { setReport, cleanup }; + } catch (error) { + await cleanup(); + throw error; + } +} + +export async function openUsage(page: Page) { + await page.goto("/usage"); +} + +export async function refreshLoginUsage(page: Page) { + await page.getByRole("button", { name: "Refresh Claude", exact: true }).click(); +} + +export async function hoverUsageWindow(page: Page, window: string) { + await page + .getByRole("checkbox", { name: new RegExp(`^Pin Claude ${window}, `) }) + .getByTestId("usage-pin-glyph-unpinned") + .hover(); +} diff --git a/packages/app/e2e/support/helpers/usage-reports.ts b/packages/app/e2e/support/helpers/usage-reports.ts index 4c853045fc..b379c27263 100644 --- a/packages/app/e2e/support/helpers/usage-reports.ts +++ b/packages/app/e2e/support/helpers/usage-reports.ts @@ -165,8 +165,13 @@ export async function installUsageReportsFixture( providerId: entry.sourceId, displayName: entry.sourceLabel, fetchedAt: entry.fetchedAt, - ...entry.report, - planLabel: entry.report.planLabel ?? null, + status: entry.report.status, + windows: entry.report.status === "available" ? entry.report.windows : [], + balances: entry.report.status === "available" ? entry.report.balances : undefined, + details: entry.report.status === "available" ? entry.report.details : undefined, + planLabel: + entry.report.status === "available" ? (entry.report.planLabel ?? null) : null, + error: entry.report.status === "error" ? entry.report.error : null, })), }, } diff --git a/packages/app/e2e/support/helpers/usage-sidebar-item.ts b/packages/app/e2e/support/helpers/usage-sidebar-item.ts index 7f73ec7897..d85ab3724f 100644 --- a/packages/app/e2e/support/helpers/usage-sidebar-item.ts +++ b/packages/app/e2e/support/helpers/usage-sidebar-item.ts @@ -36,8 +36,7 @@ import { z } from "zod"; export default function contribute(server) { server.registerUsageSource({ id: "tall-usage", label: "Scrolling account", input: z.object({}), - discover: async () => [{}], - identify: async () => ({ key: "scrolling-account" }), + discover: async () => [{key: "scrolling-account", input: {}}], fetch: async () => ({ status: "available", windows: Array.from({ length: 20 }, (_, i) => ({ id: String(i), label: "Window " + (i + 1), usedPct: 25, })) }), diff --git a/packages/app/src/usage/card.tsx b/packages/app/src/usage/card.tsx index 5175c76224..8260273e06 100644 --- a/packages/app/src/usage/card.tsx +++ b/packages/app/src/usage/card.tsx @@ -27,6 +27,19 @@ function statusText(report: UsageReport): string | null { return report.status === "error" ? "Error" : "Unavailable"; } +function reportContent(report: UsageReport) { + if (report.status === "available") + return { + windows: report.windows, + balances: report.balances ?? [], + details: report.details ?? [], + message: undefined, + }; + const message = + report.status === "unavailable" ? usageCopy.problem(report.problem) : report.error; + return { windows: [], balances: [], details: [], message }; +} + const ThemedRotateCw = withUnistyles(RotateCw); const ThemedLoadingSpinner = withUnistyles(LoadingSpinner); @@ -48,8 +61,7 @@ export function UsageCard({ const usage = entry.report; const status = statusText(usage); const footer = entry.account.label ?? null; - const balances = usage.balances ?? []; - const details = usage.details ?? []; + const { windows, balances, details, message } = reportContent(usage); const containerStyle = useMemo( () => [styles.container, compact ? styles.containerCompact : styles.containerPadded], @@ -71,7 +83,9 @@ export function UsageCard({ {entry.sourceLabel} - {usage.planLabel ? : null} + {usage.status === "available" && usage.planLabel ? ( + + ) : null} {status ? ( @@ -88,15 +102,15 @@ export function UsageCard({ /> - {usage.error ? ( + {message ? ( - {usage.error} + {message} ) : null} - {usage.windows.length > 0 || balances.length > 0 ? ( + {windows.length > 0 || balances.length > 0 ? ( - {usage.windows.map((window) => ( + {windows.map((window) => ( ))} {balances.map((balance) => ( diff --git a/packages/app/src/usage/copy.test.ts b/packages/app/src/usage/copy.test.ts new file mode 100644 index 0000000000..a5176eba93 --- /dev/null +++ b/packages/app/src/usage/copy.test.ts @@ -0,0 +1,18 @@ +import { expect, test } from "vitest"; +import { usageCopy } from "./copy"; + +test.each([ + [ + { kind: "expired", expiresAt: "2026-10-01T09:00:00Z", refreshedBy: "claude" }, + "Login expired 3h ago. Run claude to refresh it.", + ], + [{ kind: "expired", expiresAt: "2026-10-01T09:00:00Z" }, "Login expired 3h ago. Sign in again."], + [ + { kind: "rejected", status: 403, refreshedBy: "codex" }, + "Login rejected (HTTP 403). Run codex to refresh it.", + ], + [{ kind: "rejected", status: 401 }, "Login rejected (HTTP 401). Sign in again."], + [{ kind: "no_quota", detail: "No active coding plan" }, "No active coding plan"], +] as const)("problem %j", (problem, sentence) => { + expect(usageCopy.problem(problem, new Date("2026-10-01T12:00:00Z"))).toBe(sentence); +}); diff --git a/packages/app/src/usage/copy.ts b/packages/app/src/usage/copy.ts index 731450f30d..92721b69bd 100644 --- a/packages/app/src/usage/copy.ts +++ b/packages/app/src/usage/copy.ts @@ -1,6 +1,18 @@ +import type { UsageProblem } from "@getpaseo/protocol/messages"; +import { formatCompactTimeAgo, formatCompactTimeAgoAsProse } from "@/utils/time"; + // User-facing copy for the usage surfaces, kept in one file so localization is a // single-file change. export const usageCopy = { + problem: (problem: UsageProblem, now: Date = new Date()): string => { + if (problem.kind === "no_quota") return problem.detail; + const remedy = problem.refreshedBy + ? `Run ${problem.refreshedBy} to refresh it.` + : "Sign in again."; + if (problem.kind === "rejected") return `Login rejected (HTTP ${problem.status}). ${remedy}`; + const ago = formatCompactTimeAgoAsProse(formatCompactTimeAgo(new Date(problem.expiresAt), now)); + return `Login expired ${ago}. ${remedy}`; + }, title: "Usage", planUsage: "Plan usage", options: "Usage options", diff --git a/packages/app/src/usage/pinned.test.ts b/packages/app/src/usage/pinned.test.ts index 24ba59be7a..c73a8ddc34 100644 --- a/packages/app/src/usage/pinned.test.ts +++ b/packages/app/src/usage/pinned.test.ts @@ -255,3 +255,18 @@ describe("resolvePinnedUsage", () => { ]); }); }); + +it("keeps unavailable and error reports out of the sidebar summary", () => { + const expired: UsageReportEntry = { + ...claude, + report: { + status: "unavailable", + problem: { kind: "expired", expiresAt: "2026-10-01T00:00:00.000Z", refreshedBy: "claude" }, + }, + }; + const failed: UsageReportEntry = { + ...codex, + report: { status: "error", error: "Store deleted" }, + }; + expect(resolvePinnedUsage([expired, failed], preferences([]))).toEqual([]); +}); diff --git a/packages/app/src/usage/pinned.ts b/packages/app/src/usage/pinned.ts index 003d56d4e3..f05f64cdbc 100644 --- a/packages/app/src/usage/pinned.ts +++ b/packages/app/src/usage/pinned.ts @@ -66,6 +66,7 @@ function groupBySource(reports: readonly UsageReportEntry[]): UsageReportEntry[] * or its first window with a percent when the source marks none. */ function summaryWindows(entry: UsageReportEntry, preferences: UsagePreferences): UsageWindow[] { + if (entry.report.status !== "available") return []; const withPercent = entry.report.windows.filter( (window) => displayPercent(window, preferences.displayAs) !== null, ); diff --git a/packages/app/src/usage/types.ts b/packages/app/src/usage/types.ts index 2e2485b2d5..58b8e31c98 100644 --- a/packages/app/src/usage/types.ts +++ b/packages/app/src/usage/types.ts @@ -3,8 +3,9 @@ import type { ProviderUsageTone, UsageReportEntry } from "@getpaseo/protocol/mes export type { UsageReportEntry }; export type UsageReport = UsageReportEntry["report"]; export type UsageTone = ProviderUsageTone; -export type UsageWindow = UsageReport["windows"][number]; -export type UsageBalance = NonNullable[number]; +export type AvailableUsageReport = Extract; +export type UsageWindow = AvailableUsageReport["windows"][number]; +export type UsageBalance = NonNullable[number]; export type UsageBalanceUnit = UsageBalance["unit"]; /** What a host's usage surface shows. `unavailable` is a host state, not a failed request. */ diff --git a/packages/app/src/usage/window-bar.tsx b/packages/app/src/usage/window-bar.tsx index 4412836cc4..7ad06e423a 100644 --- a/packages/app/src/usage/window-bar.tsx +++ b/packages/app/src/usage/window-bar.tsx @@ -1,6 +1,11 @@ +import { Pin } from "lucide-react-native"; import { useMemo } from "react"; import { Pressable, Text, View, type StyleProp, type ViewStyle } from "react-native"; -import { StyleSheet } from "react-native-unistyles"; +import { StyleSheet, withUnistyles } from "react-native-unistyles"; +import { useIsCompactFormFactor } from "@/constants/layout"; +import { isNative } from "@/constants/platform"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; +import { usageCopy } from "./copy"; import { formatDisplayPct, formatResetLabel } from "./format"; import { UsageMeter } from "./meter"; import { displayPercent, usageWindowRowLabel } from "./model"; @@ -31,6 +36,7 @@ export function UsageWindowBar({ pinLabel: string; pinTestID: string; }) { + const isCompact = useIsCompactFormFactor(); const shownPct = displayPercent(window, displayAs); const tone = windowTone(window); @@ -63,6 +69,8 @@ export function UsageWindowBar({ isAtRisk={isAtRisk} percent={shownPct ?? 0} tone={tone} + pinVisible={Boolean(hovered) || isNative || isCompact} + pinned={pinned} /> )} @@ -77,6 +85,8 @@ function WindowRowContent({ isAtRisk, percent, tone, + pinVisible, + pinned, }: { highlight: StyleProp; label: string; @@ -85,27 +95,66 @@ function WindowRowContent({ isAtRisk: boolean; percent: number; tone: UsageTone; + pinVisible: boolean; + pinned: boolean; }) { return ( <> - - - {label} - - - {value} - {trailing ? ( - {` · ${trailing}`} - ) : null} - + + + + + {label} + + + {value} + {trailing ? ( + {` · ${trailing}`} + ) : null} + + + + + - ); } +const ThemedPin = withUnistyles(Pin); + +function UsagePinGlyph({ visible, pinned }: { visible: boolean; pinned: boolean }) { + const iconMapping = useMemo( + () => (theme: { colors: { foregroundMuted: string } }) => ({ + color: theme.colors.foregroundMuted, + fill: pinned ? theme.colors.foregroundMuted : "none", + }), + [pinned], + ); + return ( + + + + + + + + {usageCopy.pin} + + + ); +} + const styles = StyleSheet.create((theme) => ({ + contentRow: { flexDirection: "row", alignItems: "center", gap: theme.spacing[2] }, + windowContent: { flex: 1, gap: 3 }, + pin: { width: 12, alignItems: "center" }, + pinHidden: { width: 12, alignItems: "center", opacity: 0 }, + tooltipText: { color: theme.colors.popoverForeground, fontSize: theme.fontSize.sm }, row: { gap: 3, // The highlight bleeds into the card padding so the label and bar stay on the card's rail. diff --git a/packages/client/src/daemon-client.test.ts b/packages/client/src/daemon-client.test.ts index 3554fb8a0e..9870db034d 100644 --- a/packages/client/src/daemon-client.test.ts +++ b/packages/client/src/daemon-client.test.ts @@ -1,4 +1,3 @@ -import { legacyUsageIcon } from "./legacy-usage-icons.js"; import { readFileSync } from "node:fs"; import { afterEach, expect, expectTypeOf, test, vi } from "vitest"; import { z } from "zod"; @@ -6575,138 +6574,84 @@ test("sends provider.usage.list.request and resolves provider.usage.list.respons }); }); -test.each(["claude", "codex", "copilot", "cursor", "zai", "grok", "kimi", "minimax"])( - "released-host %s icon matches its source plugin", - (sourceId) => { - const icon = readFileSync( - new URL(`../../../plugins/${sourceId}-usage-source/icon.svg`, import.meta.url), - "utf8", - ); - expect(legacyUsageIcon(sourceId)).toBe(icon); - }, -); - -test("unknown released-host sources have no icon", () => { - expect(legacyUsageIcon("unknown")).toBeUndefined(); - expect(legacyUsageIcon("toString")).toBeUndefined(); -}); - -test("maps released-host usage reports and filters report IDs", async () => { - const mock = createMockTransport(); - const client = new DaemonClient({ - url: "ws://test", - clientId: "clsk_unit_test", - logger: createMockLogger(), - reconnect: { enabled: false }, - transportFactory: () => mock.transport, - }); - clients.push(client); - const connected = client.connect(); - mock.triggerOpen({ features: { providerUsageList: true } }); - await connected; - const providers = [ - { - providerId: "claude", - displayName: "Claude", +test.each([ + { + status: "available", + report: { status: "available", - planLabel: null, - fetchedAt: null, - windows: [{ id: "session", label: "Session", usedPct: 25 }], - error: null, - }, - { - providerId: "codex", - displayName: "Codex", - status: "error", - planLabel: "Pro", - fetchedAt: "2026-09-29T00:00:00.000Z", windows: [], - balances: [], - details: [], - error: "unavailable", + balances: undefined, + details: undefined, + planLabel: undefined, }, - ]; - const extraProviders = ["copilot", "cursor", "zai", "grok", "kimi", "minimax", "unknown"].map( - (providerId) => Object.assign({}, providers[0]!, { providerId, displayName: providerId }), - ); - providers.push(...extraProviders); - const reports = [ - { - id: "claude", - sourceId: "claude", - sourceLabel: "Claude", - icon: readFileSync( - new URL("../../../plugins/claude-usage-source/icon.svg", import.meta.url), - "utf8", - ), - account: {}, - fetchedAt: "2026-09-30T00:00:00.000Z", - report: { - status: "available", - planLabel: undefined, - windows: providers[0]!.windows, - balances: undefined, - details: undefined, - error: undefined, - }, - }, - { - id: "codex", - sourceId: "codex", - sourceLabel: "Codex", - icon: readFileSync( - new URL("../../../plugins/codex-usage-source/icon.svg", import.meta.url), - "utf8", - ), - account: {}, - fetchedAt: "2026-09-29T00:00:00.000Z", - report: { - status: "error", - planLabel: "Pro", - windows: [], - balances: [], - details: [], - error: "unavailable", - }, - }, - ]; - reports.push( - ...extraProviders.map((provider) => - Object.assign({}, reports[0]!, { - id: provider.providerId, - sourceId: provider.providerId, - sourceLabel: provider.displayName, - icon: legacyUsageIcon(provider.providerId), - }), - ), - ); - for (const reportIds of [undefined, ["codex", "missing"], []]) { + }, + { status: "error", report: { status: "error", error: "" } }, + { + status: "unavailable", + report: { status: "unavailable", problem: { kind: "no_quota", detail: "" } }, + }, +] as const)( + "maps released-host $status usage and filters report IDs", + async ({ status, report }) => { + const mock = createMockTransport(); + const client = new DaemonClient({ + url: "ws://test", + clientId: "clsk_unit_test", + logger: createMockLogger(), + reconnect: { enabled: false }, + transportFactory: () => mock.transport, + }); + clients.push(client); + const connected = client.connect(); + mock.triggerOpen({ features: { providerUsageList: true } }); + await connected; const result = client.listUsageReports({ requestId: "legacy-usage", - reportIds, + reportIds: ["claude"], forceRefresh: true, }); - expect(parseSentFrame(mock.sent.at(-1))).toEqual({ + expect(parseSentFrame(mock.sent[0])).toEqual({ type: "provider.usage.list.request", requestId: "legacy-usage", }); + const provider = { + providerId: "claude", + displayName: "Claude", + status, + windows: [], + planLabel: null, + fetchedAt: null, + error: null, + }; mock.triggerMessage( wrapSessionMessage({ type: "provider.usage.list.response", payload: { requestId: "legacy-usage", fetchedAt: "2026-09-30T00:00:00.000Z", - providers, + providers: [provider, { ...provider, providerId: "codex" }], }, }), ); expect(await result).toStrictEqual({ requestId: "legacy-usage", - reports: - reportIds === undefined ? reports : reports.filter((entry) => reportIds.includes(entry.id)), + reports: [ + { + id: "claude", + sourceId: "claude", + sourceLabel: "Claude", + icon: readFileSync( + new URL("../../../plugins/claude-usage-source/icon.svg", import.meta.url), + "utf8", + ), + account: {}, + fetchedAt: "2026-09-30T00:00:00.000Z", + report, + }, + ], }); - } -}); + }, +); test("sends close_items_request and resolves close_items_response", async () => { const logger = createMockLogger(); diff --git a/packages/client/src/daemon-client.ts b/packages/client/src/daemon-client.ts index 145aad7447..ab5427fe31 100644 --- a/packages/client/src/daemon-client.ts +++ b/packages/client/src/daemon-client.ts @@ -1187,8 +1187,8 @@ interface PingProbe { drivesLivenessFailure: boolean; } +// COMPAT(providerUsageList): added in v0.1.98, remove after 2027-03-26. export function supportsUsageReports(features: ServerInfoStatusPayload["features"]): boolean { - // COMPAT(providerUsageList): added in v0.1.98, remove after 2027-03-26. return features?.usageSources === true || features?.providerUsageList === true; } @@ -5278,22 +5278,36 @@ export class DaemonClient { .filter( (provider) => !options?.reportIds || options.reportIds.includes(provider.providerId), ) - .map((provider) => ({ - id: provider.providerId, - sourceId: provider.providerId, - sourceLabel: provider.displayName, - icon: legacyUsageIcon(provider.providerId), - account: {}, - fetchedAt: provider.fetchedAt ?? payload.fetchedAt, - report: { - status: provider.status, - windows: provider.windows, - balances: provider.balances ?? undefined, - details: provider.details ?? undefined, - planLabel: provider.planLabel ?? undefined, - error: provider.error ?? undefined, - }, - })), + .map((provider) => { + // COMPAT(providerUsageList): added in v0.1.98, remove after 2027-03-26. + // 0.10 reports have no typed problems; preserve their unavailable badge and error text. + let report: UsageListReportsPayload["reports"][number]["report"]; + if (provider.status === "available") { + report = { + status: "available", + windows: provider.windows, + balances: provider.balances ?? undefined, + details: provider.details ?? undefined, + planLabel: provider.planLabel ?? undefined, + }; + } else if (provider.status === "error") { + report = { status: "error", error: provider.error ?? "" }; + } else { + report = { + status: "unavailable", + problem: { kind: "no_quota", detail: provider.error ?? "" }, + }; + } + return { + id: provider.providerId, + sourceId: provider.providerId, + sourceLabel: provider.displayName, + icon: legacyUsageIcon(provider.providerId), + account: {}, + fetchedAt: provider.fetchedAt ?? payload.fetchedAt, + report, + }; + }), }; } return this.sendNamespacedCorrelatedSessionRequest({ diff --git a/packages/plugin/src/server/usage.ts b/packages/plugin/src/server/usage.ts index 11906dd955..ebf00c3b2f 100644 --- a/packages/plugin/src/server/usage.ts +++ b/packages/plugin/src/server/usage.ts @@ -38,13 +38,28 @@ export interface UsageDetail { tone?: UsageWindow["tone"]; } -export interface UsageReport { - status: "available" | "unavailable" | "error"; - planLabel?: string; - windows: UsageWindow[]; - balances?: UsageBalance[]; - details?: UsageDetail[]; - error?: string; +export type UsageProblem = + | { kind: "expired"; expiresAt: string; refreshedBy?: string } + | { kind: "rejected"; status: number; refreshedBy?: string } + | { kind: "no_quota"; detail: string }; + +export type UsageReport = + | { + status: "available"; + planLabel?: string; + windows: UsageWindow[]; + balances?: UsageBalance[]; + details?: UsageDetail[]; + } + | { status: "unavailable"; problem: UsageProblem } + | { status: "error"; error: string }; + +export interface UsageAccount { + /** Stable across token rotation; [A-Za-z0-9._-]{1,128}. Never a credential or raw email. */ + key: string; + label?: string; + /** Store locator, opaque to the daemon. */ + input: JsonValue; } export interface UsageSourceRegistration { @@ -52,10 +67,10 @@ export interface UsageSourceRegistration { label: string; icon?: string; input: ZodType; - /** Stable account identity, resolved without fetching usage. */ - identify(input: unknown): Promise<{ key: string; label?: string } | null>; + /** Every account whose login exists on this machine. Empty when none. */ + discover(): Promise; + /** Re-reads the login store; never writes it. */ fetch(input: unknown): Promise; - discover(): Promise; } export function windowFromUsedPct(input: { @@ -122,11 +137,6 @@ export function hashAccountKey(value: string): string { return createHash("sha256").update(value).digest("hex"); } -export function unavailableUsage(): UsageReport { - return { - status: "unavailable", - windows: [], - balances: [], - details: [], - }; +export function unavailable(problem: UsageProblem): UsageReport { + return { status: "unavailable", problem }; } diff --git a/packages/protocol/src/messages.ts b/packages/protocol/src/messages.ts index 861ddf4a37..a4f883604e 100644 --- a/packages/protocol/src/messages.ts +++ b/packages/protocol/src/messages.ts @@ -6245,14 +6245,30 @@ export const ProviderUsageListResponseMessageSchema = z.object({ }), }); -export const UsageReportSchema = z.object({ - status: ProviderUsageStatusSchema, - planLabel: z.string().optional(), - windows: z.array(ProviderUsageWindowSchema), - balances: z.array(ProviderUsageBalanceSchema).optional(), - details: z.array(ProviderUsageDetailSchema).optional(), - error: z.string().optional(), -}); +export const UsageProblemSchema = z.discriminatedUnion("kind", [ + z.object({ + kind: z.literal("expired"), + expiresAt: z.iso.datetime(), + refreshedBy: z.string().optional(), + }), + z.object({ + kind: z.literal("rejected"), + status: z.number().int(), + refreshedBy: z.string().optional(), + }), + z.object({ kind: z.literal("no_quota"), detail: z.string() }), +]); +export const UsageReportSchema = z.discriminatedUnion("status", [ + z.object({ + status: z.literal("available"), + planLabel: z.string().optional(), + windows: z.array(ProviderUsageWindowSchema), + balances: z.array(ProviderUsageBalanceSchema).optional(), + details: z.array(ProviderUsageDetailSchema).optional(), + }), + z.object({ status: z.literal("unavailable"), problem: UsageProblemSchema }), + z.object({ status: z.literal("error"), error: z.string() }), +]); export const UsageReportEntrySchema = z.object({ id: z.string(), account: z.object({ label: z.string().optional() }), @@ -7131,6 +7147,7 @@ export type ProviderDiagnosticResponseMessage = z.infer< typeof ProviderDiagnosticResponseMessageSchema >; export type ProviderUsageTone = z.infer; +export type UsageProblem = z.infer; export type UsageReport = z.infer; export type UsageReportEntry = z.infer; export type UsageListReportsResponseMessage = z.infer; diff --git a/packages/server/src/server/plugins/index.ts b/packages/server/src/server/plugins/index.ts index edf88de4ca..167216dbc1 100644 --- a/packages/server/src/server/plugins/index.ts +++ b/packages/server/src/server/plugins/index.ts @@ -40,7 +40,6 @@ interface PluginRuntimePort { clearLogs(pluginId: string): void; getProviderRegistrations?(pluginId: string): readonly PluginProviderMetadata[]; getUsageSourceRegistrations(pluginId: string): readonly PluginUsageSourceMetadata[]; - identifyUsage: PluginRuntime["identifyUsage"]; fetchUsage: PluginRuntime["fetchUsage"]; discoverUsage: PluginRuntime["discoverUsage"]; connectProvider: PluginRuntime["connectProvider"]; @@ -80,7 +79,7 @@ export class PluginService { private readonly errors = new Map(); private readonly listeners = new Set<(pluginId: string) => void>(); private readonly providers = new Map(); - private readonly usageSources = new UsageSourceRegistry(); + private readonly usageSources: UsageSourceRegistry; private readonly usageSourceIdsByPlugin = new Map(); private readonly providerIdsByPlugin = new Map(); private readonly providerListeners = new Set<() => void>(); @@ -96,6 +95,7 @@ export class PluginService { private readonly dependencies: PluginServiceDependencies = {}, ) { this.logger = logger.child({ module: "plugin-service" }); + this.usageSources = new UsageSourceRegistry(Date.now, 300_000, this.logger); this.runtime = dependencies.runtime ?? new PluginRuntime(logger, daemonVersion, { @@ -574,11 +574,6 @@ export class PluginService { throw new Error(`Invalid usage discovery from ${source.id}`); return result; }, - identify: (input) => - this.runtime.identifyUsage(pluginId, source.id, input) as Promise<{ - key: string; - label?: string; - } | null>, fetch: (input) => { return this.runtime.fetchUsage(pluginId, source.id, input); }, diff --git a/packages/server/src/server/plugins/plugin-process-protocol.ts b/packages/server/src/server/plugins/plugin-process-protocol.ts index 6ae2cf140f..9b543b2a75 100644 --- a/packages/server/src/server/plugins/plugin-process-protocol.ts +++ b/packages/server/src/server/plugins/plugin-process-protocol.ts @@ -51,7 +51,6 @@ export type PluginProcessRequest = } | { type: "hook"; requestId: string; kind: "event" | "before"; name: string; input: unknown } | { type: "hook.cancel"; requestId: string } - | { type: "usage.identify"; requestId: string; sourceId: string; input: unknown } | { type: "usage.fetch"; requestId: string; sourceId: string; input: unknown } | { type: "usage.discover"; requestId: string; sourceId: string } | { type: "invoke"; requestId: string; method: string; input: unknown } @@ -191,14 +190,6 @@ export const PluginProcessRequestSchema: z.ZodType = z.dis }) .strict(), z.object({ type: z.literal("hook.cancel"), requestId: z.string() }).strict(), - z - .object({ - type: z.literal("usage.identify"), - requestId: z.string(), - sourceId: z.string(), - input: z.unknown(), - }) - .strict(), z .object({ type: z.literal("usage.fetch"), diff --git a/packages/server/src/server/plugins/plugin-process.ts b/packages/server/src/server/plugins/plugin-process.ts index ee1d17310a..93c9a3d922 100644 --- a/packages/server/src/server/plugins/plugin-process.ts +++ b/packages/server/src/server/plugins/plugin-process.ts @@ -339,21 +339,14 @@ export function createPluginWorker(options: { } function handleUsageRequest( - message: Extract< - PluginProcessRequest, - { type: "usage.identify" | "usage.fetch" | "usage.discover" } - >, + message: Extract, ): void { void (async () => { const source = usageSources.get(message.sourceId); if (!source) throw new Error(`Unknown usage source: ${message.sourceId}`); if (message.type === "usage.discover") return jsonTransportValue(await source.discover()); const input = await source.input.parseAsync(message.input); - return jsonTransportValue( - message.type === "usage.identify" - ? await source.identify(input) - : await source.fetch(input), - ); + return jsonTransportValue(await source.fetch(input)); })().then( (output) => send({ type: "result", requestId: message.requestId, output }), (error) => send({ type: "error", requestId: message.requestId, error: describeError(error) }), @@ -364,7 +357,6 @@ export function createPluginWorker(options: { if ( message.type === "provider.status" || message.type === "provider.catalog_key" || - message.type === "usage.identify" || message.type === "usage.fetch" || message.type === "usage.discover" ) { @@ -453,11 +445,7 @@ export function createPluginWorker(options: { ); return; } - if ( - message.type === "usage.identify" || - message.type === "usage.fetch" || - message.type === "usage.discover" - ) { + if (message.type === "usage.fetch" || message.type === "usage.discover") { handleUsageRequest(message); return; } diff --git a/packages/server/src/server/plugins/runtime.posix.test.ts b/packages/server/src/server/plugins/runtime.posix.test.ts index 5615bef6ac..8dd80ce499 100644 --- a/packages/server/src/server/plugins/runtime.posix.test.ts +++ b/packages/server/src/server/plugins/runtime.posix.test.ts @@ -2040,7 +2040,6 @@ export default function contribute(server) { id: "missing-discovery", label: "Missing discovery", input: { parseAsync: async (value) => value }, - identify: async () => ({ key: "account" }), fetch: async () => ({ status: "available", windows: [] }), }); return () => {}; diff --git a/packages/server/src/server/plugins/runtime.ts b/packages/server/src/server/plugins/runtime.ts index 9db0853718..a26e7cbfea 100644 --- a/packages/server/src/server/plugins/runtime.ts +++ b/packages/server/src/server/plugins/runtime.ts @@ -395,17 +395,6 @@ export class PluginRuntime { return this.plugins.get(pluginId)?.usageSources ?? []; } - identifyUsage(pluginId: string, sourceId: string, input: unknown): Promise { - const loaded = this.plugins.get(pluginId); - if (!loaded) throw new Error(`Plugin is not available: ${pluginId}`); - return this.request(loaded, { - type: "usage.identify", - requestId: randomUUID(), - sourceId, - input, - }); - } - fetchUsage(pluginId: string, sourceId: string, input: unknown): Promise { const loaded = this.plugins.get(pluginId); if (!loaded) throw new Error(`Plugin is not available: ${pluginId}`); diff --git a/packages/server/src/server/plugins/test-fixtures/usage-source-directory/index.server.ts b/packages/server/src/server/plugins/test-fixtures/usage-source-directory/index.server.ts index 5845073a7c..0663e0070d 100644 --- a/packages/server/src/server/plugins/test-fixtures/usage-source-directory/index.server.ts +++ b/packages/server/src/server/plugins/test-fixtures/usage-source-directory/index.server.ts @@ -13,12 +13,21 @@ export default function contribute(server: PluginServerContext) { { account: "one" }, { account: "bad", extra: true }, { account: "throws" }, + { account: "expired" }, ]; - return inputs; + return inputs.map((input) => ({ key: String(input.account), input })); }, - identify: async (input) => ({ key: (input as { account: string }).account }), fetch: async (input) => { const account = (input as { account: string }).account; + if (account === "expired") + return { + status: "unavailable", + problem: { + kind: "expired", + expiresAt: new Date(Date.now() - 3_600_000).toISOString(), + refreshedBy: "claude", + }, + }; if (account === "throws") throw new Error("fixture failure"); fetches++; return { diff --git a/packages/server/src/server/plugins/test-fixtures/usage-source/index.server.ts b/packages/server/src/server/plugins/test-fixtures/usage-source/index.server.ts index 347e8050d0..12d7ced848 100644 --- a/packages/server/src/server/plugins/test-fixtures/usage-source/index.server.ts +++ b/packages/server/src/server/plugins/test-fixtures/usage-source/index.server.ts @@ -13,12 +13,21 @@ export default function contribute(server: PluginServerContext) { { account: "one" }, { account: "bad", extra: true }, { account: "throws" }, + { account: "expired" }, ]; - return inputs; + return inputs.map((input) => ({ key: String(input.account), input })); }, - identify: async (input) => ({ key: (input as { account: string }).account }), fetch: async (input) => { const account = (input as { account: string }).account; + if (account === "expired") + return { + status: "unavailable", + problem: { + kind: "expired", + expiresAt: new Date(Date.now() - 3_600_000).toISOString(), + refreshedBy: "claude", + }, + }; if (account === "throws") throw new Error("fixture failure"); fetches++; return { diff --git a/packages/server/src/server/plugins/usage-sources.e2e.test.ts b/packages/server/src/server/plugins/usage-sources.e2e.test.ts index df81a8fbb2..0b2b3ca9a2 100644 --- a/packages/server/src/server/plugins/usage-sources.e2e.test.ts +++ b/packages/server/src/server/plugins/usage-sources.e2e.test.ts @@ -22,7 +22,7 @@ test("lists built-in and subprocess usage; validates input and isolates fetch er try { await client.connect(); const first = await client.listUsageReports(); - expect(first.reports).toHaveLength(3); + expect(first.reports).toHaveLength(4); expect(first.reports[0]?.icon).toContain(" entry.id === "fixture:expired"); + expect(expired?.report).toEqual({ + status: "unavailable", + problem: { kind: "expired", expiresAt: expect.any(String), refreshedBy: "claude" }, + }); + const legacy = await client.listProviderUsage(); + expect(legacy.providers.find((provider) => provider.displayName === "Fixture")?.status).toBe( + "available", + ); + expect(legacy.providers.find((provider) => provider.status === "unavailable")?.error).toBe( + "Login expired 1h ago. Run claude to refresh it.", + ); await client.patchDaemonConfig({ pluginsEnabled: true }); await client.installDirectoryPlugin(subprocessDirectory, "fixture-directory"); const both = await client.listUsageReports({ forceRefresh: true }); - expect(both.reports).toHaveLength(6); + expect(both.reports).toHaveLength(8); await client.patchDaemonConfig({ pluginsEnabled: false }); - await expect.poll(async () => (await client.listUsageReports()).reports.length).toBe(3); + await expect.poll(async () => (await client.listUsageReports()).reports.length).toBe(4); } finally { await client.close(); await daemon.close(); @@ -81,8 +93,7 @@ import { account } from './server/account.js'; export default function contribute(server) { server.registerUsageSource({ id: 'fixture', label: 'Fixture', input: z.object({}), - discover: async () => [{}], - identify: async () => account, + discover: async () => [{...account, input: {}}], fetch: async () => ({ status: 'available', windows: [{ id: 'session', label: 'Session', usedPct: 37 }] }), }); return () => {}; @@ -183,16 +194,12 @@ test("built-in sources deduplicate cross-harness accounts and fall back without await cp(path.join(resolveBuiltinPluginsRoot(), `${source}-usage-source`), directory, { recursive: true, }); - const identifyCall = - source === "codex" - ? "identify(inputSchema.parse(input), lookup)" - : "identify(inputSchema.parse(input), fetchApi, Date.now, lookup)"; // Inject home/env/HTTP at the source boundary; compile and register the real readers. await writeFile( path.join(directory, "index.server.ts"), ` import { inputSchema } from './shared/input.js'; -import { discover, identify, fetchUsage } from './server/usage.js'; +import { discover, fetchUsage } from './server/usage.js'; const lookup = { home: ${JSON.stringify(home)}, env: ${JSON.stringify(env)}, platform: 'linux' }; async function fetchApi(url, init) { const token = new Headers(init.headers).get('Authorization'); @@ -206,8 +213,7 @@ async function fetchApi(url, init) { } export default function contribute(server) { server.registerUsageSource({ id: '${source}', label: '${source}', input: inputSchema, - discover: () => discover(lookup), - identify: (input) => ${identifyCall}, + discover: () => discover(lookup${source === "claude" ? ", fetchApi" : ""}), fetch: (input) => fetchUsage(inputSchema.parse(input), fetchApi, lookup), }); return () => {}; @@ -268,3 +274,52 @@ export default function contribute(server) { await rm(root, { recursive: true, force: true }); } }, 60_000); + +test("a killed discovery subprocess logs kimi once and produces no provider card", async () => { + const { readFile } = await import("node:fs/promises"); + const { default: pino } = await import("pino"); + const root = await mkdtemp(path.join(os.tmpdir(), "usage-crash-")); + const log = path.join(root, "daemon.log"); + const destination = pino.destination({ dest: log, sync: true }); + const daemon = await createTestPaseoDaemon({ + daemonVersion: "0.9.2", + pluginsEnabled: true, + logger: pino({ level: "warn" }, destination), + }); + const client = new DaemonClient({ url: `ws://127.0.0.1:${daemon.port}/ws`, appVersion: "0.9.2" }); + try { + await writeFile( + path.join(root, "paseo-plugin.json"), + JSON.stringify({ id: "crashing-usage", requirements: { paseo: ">=0.9.2" } }), + ); + await writeFile( + path.join(root, "index.server.ts"), + ` +import {z} from "zod"; +export default function contribute(server) { + server.registerUsageSource({id: "kimi", label: "Kimi", input: z.object({}), + discover: async () => {process.exit(1);}, + fetch: async () => {throw new Error("discovery must never produce an account");}, + }); + return () => {}; +}`, + ); + await client.connect(); + await client.installDirectoryPlugin(root, "crashing-usage"); + expect((await client.listUsageReports({ forceRefresh: true })).reports).toEqual([]); + const warnings = (await readFile(log, "utf8")) + .trim() + .split("\n") + .map((line) => JSON.parse(line)) + .filter((entry) => entry.msg === "Usage source discovery failed"); + expect(warnings.map(({ level, sourceId }) => ({ level, sourceId }))).toEqual([ + { level: 40, sourceId: "kimi" }, + ]); + expect((await client.listUsageReports({ forceRefresh: true })).reports).toEqual([]); + } finally { + await client.close(); + await daemon.close(); + destination.end(); + await rm(root, { recursive: true, force: true }); + } +}, 60_000); diff --git a/packages/server/src/server/plugins/usage-sources/index.test.ts b/packages/server/src/server/plugins/usage-sources/index.test.ts index 64fd87c566..17ad174a29 100644 --- a/packages/server/src/server/plugins/usage-sources/index.test.ts +++ b/packages/server/src/server/plugins/usage-sources/index.test.ts @@ -3,17 +3,13 @@ import { UsageSourceRegistry } from "./index.js"; function source(input: { id: string; - discover?: () => Promise; - identify?: (value: unknown) => Promise<{ key: string; label?: string } | null>; + discover?: () => Promise>; fetch?: (value: unknown) => Promise; }) { return { id: input.id, label: input.id, discover: input.discover ?? (async () => []), - identify: - input.identify ?? - (async (value: unknown) => ({ key: (value as { account: string }).account })), fetch: input.fetch ?? (async () => ({ status: "available", windows: [] })), }; } @@ -24,7 +20,7 @@ test("discovery preserves account IDs and updates input after token rotation", a registry.register( source({ id: "codex", - discover: async () => [{ account: "work", token }], + discover: async () => [{ key: "work", input: { token } }], fetch: async (input) => ({ status: "available", windows: [{ id: "token", label: (input as { token: string }).token }], @@ -47,7 +43,7 @@ test("coalesces per account, caches errors, and refreshes only requested IDs", a registry.register( source({ id: "source", - discover: async () => [{ account: "a" }, { account: "b" }], + discover: async () => ["a", "b"].map((account) => ({ key: account, input: { account } })), fetch: async (input) => { const account = (input as { account: string }).account; counts.set(account, (counts.get(account) ?? 0) + 1); @@ -76,28 +72,14 @@ test("coalesces per account, caches errors, and refreshes only requested IDs", a expect(counts.get("a")).toBe(3); }); -test("invalid keys become source errors and missing identities produce no report", async () => { - const registry = new UsageSourceRegistry(); - registry.register( - source({ - id: "source", - discover: async () => [{ account: "bad:key" }, { account: "none" }], - identify: async (input) => { - const account = (input as { account: string }).account; - return account === "none" ? null : { key: account }; - }, - }), - ); - const reports = await registry.listReports(); - expect(reports.map((entry) => entry.id)).toEqual(["source:!error"]); - expect(reports[0]?.report.status).toBe("error"); -}); - test("legacy listing uses oldest fetchedAt", async () => { let now = 1000; const registry = new UsageSourceRegistry(() => now); registry.register( - source({ id: "source", discover: async () => [{ account: "a" }, { account: "b" }] }), + source({ + id: "source", + discover: async () => ["a", "b"].map((account) => ({ key: account, input: { account } })), + }), ); await registry.listReports(); now = 2000; @@ -115,7 +97,7 @@ test("concurrent requests for the same ID share one vendor fetch", async () => { registry.register( source({ id: "coalesced", - discover: async () => [{ account: "one" }], + discover: async () => [{ key: "one", input: {} }], fetch: async () => { fetches++; return response; @@ -130,49 +112,38 @@ test("concurrent requests for the same ID share one vendor fetch", async () => { expect(fetches).toBe(1); }); -test("source failure IDs cannot collide with an account named error", async () => { - const registry = new UsageSourceRegistry(); - registry.register( - source({ - id: "source", - discover: async () => [{ account: "error" }, { account: "bad:key" }], - }), - ); - const reports = await registry.listReports(); - expect(reports).toHaveLength(2); - expect(reports.find((entry) => entry.id === "source:error")?.report.status).toBe("available"); - expect(reports.find((entry) => entry.report.status === "error")?.id).toMatch( - /^source:[^A-Za-z0-9._-]/, - ); -}); - -test("expired cached entries are pruned when a new report is written", async () => { +test("rediscovery removes an old account from targeted refreshes", async () => { let now = 0; const registry = new UsageSourceRegistry(() => now, 100); let account = "old"; - registry.register(source({ id: "source", discover: async () => [{ account }] })); + registry.register(source({ id: "source", discover: async () => [{ key: account, input: {} }] })); await registry.listReports(); await registry.listReports({ reportIds: ["source:old"] }); now = 101; account = "new"; await registry.listReports(); await registry.listReports({ reportIds: ["source:new"] }); - const cache = Reflect.get(registry, "cache") as Map; - expect([...cache.keys()]).toEqual(["source:new"]); + expect(await registry.listReports({ reportIds: ["source:old"] })).toEqual([]); }); -test("discovery failures have an ID outside the account namespace", async () => { - const registry = new UsageSourceRegistry(); +test("discovery failures log once and produce no card", async () => { + const warnings: unknown[][] = []; + const registry = new UsageSourceRegistry(Date.now, 300_000, { + warn: (...args: unknown[]) => { + warnings.push(args); + }, + }); + const err = new Error("subprocess stopped"); registry.register( source({ - id: "source", + id: "kimi", discover: async () => { - throw new Error("discovery failed"); + throw err; }, }), ); - const discovered = await registry.listReports(); - expect(discovered.map((entry) => entry.id)).toEqual(["source:!error"]); + expect(await registry.listReports()).toEqual([]); + expect(warnings).toEqual([[{ sourceId: "kimi", err }, "Usage source discovery failed"]]); }); test("legacy listing distinguishes labeled accounts and preserves unlabeled names", async () => { @@ -180,11 +151,12 @@ test("legacy listing distinguishes labeled accounts and preserves unlabeled name registry.register( source({ id: "claude", - discover: async () => [{ account: "work" }, { account: "personal" }, { account: "default" }], - identify: async (input) => { - const { account } = input as { account: string }; - return { key: account, label: account === "default" ? undefined : account }; - }, + discover: async () => + ["work", "personal", "default"].map((key) => ({ + key, + label: key === "default" ? undefined : key, + input: {}, + })), }), ); expect( @@ -199,16 +171,14 @@ test("duplicate logins follow discovery order and fall back on unavailable, erro registry.register( source({ id: "codex", - discover: async () => [0, 1, 2], - identify: async (input) => { - if (input === 0) await new Promise((resolve) => setTimeout(resolve, 10)); - return { key: "same" }; - }, + discover: async () => [0, 1, 2].map((input) => ({ key: "same", input })), fetch: async (input) => { calls.push(Number(input)); if (input === 0) { if (failure === "throw") throw new Error("revoked"); - return { status: failure, windows: [] }; + return failure === "unavailable" + ? { status: failure, problem: { kind: "rejected", status: 401 } } + : { status: failure, error: "revoked" }; } return { status: "available", @@ -224,56 +194,88 @@ test("duplicate logins follow discovery order and fall back on unavailable, erro } }); -test("targeted refresh skips a login that changed accounts and preserves fallback", async () => { - let firstAccount = "original"; - const fetched: unknown[] = []; +test("targeted refresh re-reads the login without discovering again", async () => { + let discoveries = 0; + let value = "old"; const registry = new UsageSourceRegistry(); registry.register( source({ id: "source", - discover: async () => [0, 1], - identify: async (input) => ({ key: input === 0 ? firstAccount : "original" }), - fetch: async (input) => { - fetched.push(input); - return { status: "available", windows: [] }; + discover: async () => { + discoveries++; + return [{ key: "original", input: {} }]; }, + fetch: async () => ({ status: "available", windows: [{ id: "session", label: value }] }), }), ); await registry.listReports(); - firstAccount = "different"; + value = "new"; const reports = await registry.listReports({ reportIds: ["source:original"], forceRefresh: true, }); - expect(reports[0]?.id).toBe("source:original"); - expect(reports[0]?.report.status).toBe("available"); - expect(fetched).toEqual([0, 1]); + expect(reports[0]?.report).toEqual({ + status: "available", + windows: [{ id: "session", label: "new" }], + }); + expect(discoveries).toBe(1); }); -test("discovery identifies a source's logins concurrently and keeps their order", async () => { +test("a thrown fetch is an error card", async () => { const registry = new UsageSourceRegistry(); - const fetched: unknown[] = []; - let release!: () => void; - const allStarted = new Promise((resolve) => (release = resolve)); - let started = 0; registry.register( source({ - id: "claude", - discover: async () => [0, 1, 2], - // Each identification waits until all three started; serial identification never finishes. - identify: async (input) => { - if (++started === 3) release(); - await allStarted; - if (input === 0) await new Promise((resolve) => setTimeout(resolve, 10)); - return { key: "same" }; - }, - fetch: async (input) => { - fetched.push(input); - return { status: "unavailable", windows: [] }; + id: "source", + discover: async () => [{ key: "one", input: {} }], + fetch: async () => { + throw new Error("Store deleted"); }, }), ); - const reports = await registry.listReports(); - expect(reports.map((entry) => entry.id)).toEqual(["claude:same"]); - expect(fetched).toEqual([0, 1, 2]); + expect((await registry.listReports())[0]?.report).toEqual({ + status: "error", + error: "Store deleted", + }); +}); + +test("an unavailable login falls back to the next available login", async () => { + const calls: unknown[] = []; + const registry = new UsageSourceRegistry(); + registry.register( + source({ + id: "source", + discover: async () => [1, 2].map((input) => ({ key: "same", input })), + fetch: async (input) => { + calls.push(input); + return input === 1 + ? { + status: "unavailable", + problem: { kind: "expired", expiresAt: "1970-01-01T00:00:00.000Z" }, + } + : { status: "available", windows: [] }; + }, + }), + ); + expect( + (await registry.listReports()).map((entry) => ({ id: entry.id, report: entry.report })), + ).toEqual([{ id: "source:same", report: { status: "available", windows: [] } }]); + expect(calls).toEqual([1, 2]); +}); + +test("failed fallbacks preserve the final problem", async () => { + const registry = new UsageSourceRegistry(); + registry.register( + source({ + id: "source", + discover: async () => [1, 2].map((input) => ({ key: "same", input })), + fetch: async (input) => ({ + status: "unavailable", + problem: { kind: "rejected", status: input === 1 ? 401 : 403 }, + }), + }), + ); + expect((await registry.listReports())[0]?.report).toEqual({ + status: "unavailable", + problem: { kind: "rejected", status: 403 }, + }); }); diff --git a/packages/server/src/server/plugins/usage-sources/index.ts b/packages/server/src/server/plugins/usage-sources/index.ts index bdc5c344d3..2d31798fbd 100644 --- a/packages/server/src/server/plugins/usage-sources/index.ts +++ b/packages/server/src/server/plugins/usage-sources/index.ts @@ -1,21 +1,24 @@ +import { z } from "zod"; +import type { Logger } from "pino"; import { UsageReportSchema, type ProviderUsage, type UsageReportEntry, + type UsageProblem, + type UsageReport, } from "@getpaseo/protocol/messages"; export interface UsageSource { id: string; label: string; icon?: string; - discover(): Promise; - identify(input: unknown): Promise<{ key: string; label?: string } | null>; + discover(): Promise; fetch(input: unknown): Promise; } interface KnownReport { source: UsageSource; - inputs: unknown[]; + inputs: [unknown, ...unknown[]]; label?: string; } @@ -29,6 +32,7 @@ export class UsageSourceRegistry { constructor( private readonly now: () => number = Date.now, private readonly ttlMs = 300_000, + private readonly logger: Pick = { warn: console.warn }, ) {} register(source: UsageSource): void { @@ -42,36 +46,15 @@ export class UsageSourceRegistry { for (const key of this.cache.keys()) if (key.startsWith(`${id}:`)) this.cache.delete(key); } - private async identify( - source: UsageSource, - input: unknown, - ): Promise<{ id: string; label?: string; error?: true } | null> { - try { - const account = await source.identify(input); - if (account === null) return null; - if ( - typeof account !== "object" || - (account.label !== undefined && typeof account.label !== "string") - ) - throw new Error(`Invalid account identity from ${source.id}`); - if (typeof account.key !== "string" || !/^[A-Za-z0-9._-]{1,128}$/.test(account.key)) - throw new Error(`Invalid account key from ${source.id}`); - return { id: `${source.id}:${account.key}`, label: account.label }; - } catch (error) { - const id = `${source.id}:!error`; - this.writeCache(id, this.errorEntry(source, id, error)); - return { id, error: true }; - } - } - async listReports( options: { forceRefresh?: boolean; reportIds?: string[] } = {}, ): Promise { const ids = options.reportIds ?? (await this.discoverReportIds()); return Promise.all( - [...new Set(ids)] - .filter((id) => this.known.has(id) || this.cache.has(id)) - .map((id) => this.fetchId(id, options.forceRefresh)), + [...new Set(ids)].flatMap((id) => { + const known = this.known.get(id); + return known ? [this.fetchId(id, known, options.forceRefresh)] : []; + }), ); } @@ -79,34 +62,35 @@ export class UsageSourceRegistry { const discovered = await Promise.all( [...this.sources.values()].map(async (source) => { try { - const inputs = await source.discover(); - if (!Array.isArray(inputs)) throw new Error("Usage discovery must return an array"); - const accounts = await Promise.all(inputs.map((input) => this.identify(source, input))); - // Group in discovery order so an account's logins keep the source's preference. + const accounts = z + .array( + z.object({ + key: z.string().regex(/^[A-Za-z0-9._-]{1,128}$/), + label: z.string().optional(), + input: z.json(), + }), + ) + .parse(await source.discover()); const knownReports = new Map(); const ids: string[] = []; - accounts.forEach((account, index) => { - if (account === null) return; - ids.push(account.id); - if (account.error) return; - const known = knownReports.get(account.id); - if (known) known.inputs.push(inputs[index]); - else - knownReports.set(account.id, { - source, - inputs: [inputs[index]], - label: account.label, - }); - }); + for (const account of accounts) { + const id = `${source.id}:${account.key}`; + ids.push(id); + const known = knownReports.get(id); + if (known) known.inputs.push(account.input); + else knownReports.set(id, { source, inputs: [account.input], label: account.label }); + } for (const key of this.known.keys()) { if (key.startsWith(`${source.id}:`)) this.known.delete(key); } for (const [id, known] of knownReports) this.known.set(id, known); return ids; } catch (error) { - const id = `${source.id}:!error`; - this.writeCache(id, this.errorEntry(source, id, error)); - return [id]; + for (const key of this.known.keys()) { + if (key.startsWith(`${source.id}:`)) this.known.delete(key); + } + this.logger.warn({ sourceId: source.id, err: error }, "Usage source discovery failed"); + return []; } }), ); @@ -129,45 +113,31 @@ export class UsageSourceRegistry { ? `${entry.sourceLabel} (${entry.account.label})` : entry.sourceLabel, status: entry.report.status, - planLabel: entry.report.planLabel ?? null, - windows: entry.report.windows, - balances: entry.report.balances ?? [], - details: entry.report.details ?? [], - error: entry.report.error ?? null, + planLabel: entry.report.status === "available" ? (entry.report.planLabel ?? null) : null, + windows: entry.report.status === "available" ? entry.report.windows : [], + balances: entry.report.status === "available" ? (entry.report.balances ?? []) : [], + details: entry.report.status === "available" ? (entry.report.details ?? []) : [], + error: legacyError(entry.report, this.now()), })), }; } - private fetchId(id: string, forceRefresh = false): Promise { - const known = this.known.get(id); + private fetchId(id: string, known: KnownReport, forceRefresh = false): Promise { const cached = this.cache.get(id); - if (!known) return Promise.resolve(cached!.entry); if (!forceRefresh && cached && this.now() - cached.at < this.ttlMs) return Promise.resolve(cached.entry); const pending = this.pending.get(id); if (pending) return pending; const request = (async () => { - let entry: UsageReportEntry = { + const entry: UsageReportEntry = { id, sourceId: known.source.id, sourceLabel: known.source.label, icon: known.source.icon, account: { label: known.label }, fetchedAt: new Date(this.now()).toISOString(), - report: { status: "unavailable", windows: [] }, + report: await this.fetchWithFallback(known), }; - for (const input of known.inputs) { - try { - // A harness can switch accounts between discovery and a targeted refresh. - const account = await known.source.identify(input); - if (!account || `${known.source.id}:${account.key}` !== id) continue; - const report = UsageReportSchema.parse(await known.source.fetch(input)); - entry.report = report; - if (report.status === "available") break; - } catch (error) { - entry = this.errorEntry(known.source, id, error, known.label); - } - } this.writeCache(id, entry); return entry; })(); @@ -186,25 +156,43 @@ export class UsageSourceRegistry { this.cache.set(id, { at, entry }); } - private errorEntry( - source: UsageSource, - id: string, - error: unknown, - label?: string, - fetchedAt = new Date(this.now()).toISOString(), - ): UsageReportEntry { - return { - id, - sourceId: source.id, - sourceLabel: source.label, - icon: source.icon, - account: { label }, - fetchedAt, - report: { - status: "error", - windows: [], - error: error instanceof Error ? error.message : String(error), - }, - }; + private async fetchWithFallback(known: KnownReport): Promise { + let report = await this.fetchLogin(known.source, known.inputs[0]); + for (const input of known.inputs.slice(1)) { + if (report.status === "available") break; + report = await this.fetchLogin(known.source, input); + } + return report; + } + + private async fetchLogin(source: UsageSource, input: unknown): Promise { + try { + return UsageReportSchema.parse(await source.fetch(input)); + } catch (error) { + return { status: "error", error: error instanceof Error ? error.message : String(error) }; + } } } + +// The legacy RPC has only an error string; the current app owns its own presentation. +// COMPAT(providerUsageList): added in v0.1.98, remove after 2027-03-26. +function legacyProblem(problem: UsageProblem, now: number): string { + if (problem.kind === "no_quota") return problem.detail; + const remedy = problem.refreshedBy + ? `Run ${problem.refreshedBy} to refresh it.` + : "Sign in again."; + if (problem.kind === "rejected") return `Login rejected (HTTP ${problem.status}). ${remedy}`; + const elapsed = Math.max(0, now - Date.parse(problem.expiresAt)); + let ago = "just now"; + if (elapsed >= 86_400_000) ago = `${Math.floor(elapsed / 86_400_000)}d ago`; + else if (elapsed >= 3_600_000) ago = `${Math.floor(elapsed / 3_600_000)}h ago`; + else if (elapsed >= 60_000) ago = `${Math.floor(elapsed / 60_000)}m ago`; + return `Login expired ${ago}. ${remedy}`; +} + +// COMPAT(providerUsageList): added in v0.1.98, remove after 2027-03-26. +function legacyError(report: UsageReport, now: number): string | null { + if (report.status === "error") return report.error; + if (report.status === "unavailable") return legacyProblem(report.problem, now); + return null; +} diff --git a/plugins/claude-usage-source/index.server.ts b/plugins/claude-usage-source/index.server.ts index fae161db58..82bcae58b9 100644 --- a/plugins/claude-usage-source/index.server.ts +++ b/plugins/claude-usage-source/index.server.ts @@ -1,6 +1,6 @@ import type { PluginServerContext } from "@getpaseo/plugin/server"; import { inputSchema } from "./shared/input.js"; -import { discover, fetchUsage, identify } from "./server/usage.js"; +import { discover, fetchUsage } from "./server/usage.js"; export default function contribute(server: PluginServerContext) { server.registerUsageSource({ @@ -9,7 +9,6 @@ export default function contribute(server: PluginServerContext) { icon: "icon.svg", input: inputSchema, discover: () => discover(), - identify, fetch: fetchUsage, }); return () => {}; diff --git a/plugins/claude-usage-source/server/keychain.test.ts b/plugins/claude-usage-source/server/keychain.test.ts index 8f5ee0bd99..f79153933f 100644 --- a/plugins/claude-usage-source/server/keychain.test.ts +++ b/plugins/claude-usage-source/server/keychain.test.ts @@ -90,11 +90,19 @@ import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { discover, fetchUsage } from "./usage.js"; +import { inputSchema } from "../shared/input.js"; function fixtureFetch(expectedToken: string): typeof fetch { return vi.fn(async (_url: RequestInfo | URL, init?: RequestInit) => { expect(init?.headers).toMatchObject({ Authorization: `Bearer ${expectedToken}` }); - return new Response(JSON.stringify({ five_hour: { utilization: 12 } }), { status: 200 }); + return new Response( + JSON.stringify({ + account: { uuid: "fixture-account" }, + organization: { uuid: "fixture-org" }, + five_hour: { utilization: 12 }, + }), + { status: 200 }, + ); }) as unknown as typeof fetch; } @@ -125,30 +133,33 @@ describe("Claude credential routes", () => { join(dir, ".credentials.json"), JSON.stringify({ claudeAiOauth: { accessToken: "fixture-default" } }), ); - const [input] = await discover({ - home: dir, - env: { CLAUDE_CONFIG_DIR: dir }, - platform: "linux", - }); - expect(input).toEqual({ route: { store: "claude", path: join(dir, ".credentials.json") } }); - expect((await fetchUsage(input!, fixtureFetch("fixture-default"))).status).toBe("available"); + const accounts = await discover( + { home: dir, env: { CLAUDE_CONFIG_DIR: dir }, platform: "linux" }, + fixtureFetch("fixture-default"), + ); + expect(accounts).toEqual([ + { + key: "fixture-account.fixture-org", + input: { route: { store: "claude", path: join(dir, ".credentials.json") } }, + }, + ]); + expect( + (await fetchUsage(inputSchema.parse(accounts[0]!.input), fixtureFetch("fixture-default"))) + .status, + ).toBe("available"); } finally { rmSync(dir, { recursive: true, force: true }); } }); - it("stays unavailable when the routed file has no credentials", async () => { + it("throws without fetching when the routed file has no credentials", async () => { const dir = mkdtempSync(join(tmpdir(), "claude-empty-config-")); try { const fetchApi = vi.fn() as unknown as typeof fetch; - expect( - ( - await fetchUsage( - { route: { store: "claude", path: join(dir, ".credentials.json") } }, - fetchApi, - { platform: "linux" }, - ) - ).status, - ).toBe("unavailable"); + await expect( + fetchUsage({ route: { store: "claude", path: join(dir, ".credentials.json") } }, fetchApi, { + platform: "linux", + }), + ).rejects.toThrow("Claude login store no longer exists"); expect(fetchApi).not.toHaveBeenCalled(); } finally { rmSync(dir, { recursive: true, force: true }); diff --git a/plugins/claude-usage-source/server/stores.test.ts b/plugins/claude-usage-source/server/stores.test.ts index 7e0392b88b..2f8119f502 100644 --- a/plugins/claude-usage-source/server/stores.test.ts +++ b/plugins/claude-usage-source/server/stores.test.ts @@ -2,7 +2,7 @@ import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, beforeEach, expect, test } from "vitest"; -import { discover, fetchUsage, identify } from "./usage.js"; +import { discover, fetchUsage } from "./usage.js"; let home: string; beforeEach(async () => { @@ -25,8 +25,21 @@ async function profile(_url: RequestInfo | URL, init?: RequestInit) { JSON.stringify({ account: { uuid: "pi-account" }, organization: { uuid: "pi-org" } }), ); } -async function account(input: Parameters[0], env: NodeJS.ProcessEnv = {}) { - return identify(input, profile, () => 1000, lookup(env)); +async function accountIdentity( + input: Parameters[0], + env: NodeJS.ProcessEnv = {}, +) { + const accounts = await discover(lookup(env), profile); + const account = accounts.find( + (candidate) => JSON.stringify(candidate.input) === JSON.stringify(input), + ); + if (!account) throw new Error("Expected discovered login"); + return { key: account.key, ...(account.label ? { label: account.label } : {}) }; +} +async function logins(options: Parameters[0]) { + return (await discover(options, profile)).map( + (account) => account.input as Parameters[0], + ); } async function database(path: string) { await mkdir((await import("node:path")).dirname(path), { recursive: true }); @@ -76,9 +89,9 @@ test("Pi uses its own OAuth identity and re-reads rotated tokens without writing await json(join(home, ".claude.json"), { oauthAccount: { accountUuid: "other", organizationUuid: "other-org" }, }); - const inputs = await discover(lookup()); + const inputs = await logins(lookup()); expect(inputs).toHaveLength(1); - expect(await account(inputs[0]!)).toEqual({ key: "pi-account.pi-org" }); + expect(await accountIdentity(inputs[0]!)).toEqual({ key: "pi-account.pi-org" }); await json(path, { anthropic: { type: "oauth", access: "fixture-rotated", accountId: "pi-account" }, }); @@ -103,18 +116,18 @@ test("Pi honors directory override and ignores API keys and malformed files", as const env = { PI_CODING_AGENT_DIR: directory }; const path = join(directory, "auth.json"); await json(path, { anthropic: { type: "oauth", access: "fixture-pi", accountId: "pi-account" } }); - expect(await discover(lookup(env))).toEqual([{ route: { store: "pi", path } }]); + expect(await logins(lookup(env))).toEqual([{ route: { store: "pi", path } }]); await json(path, { anthropic: { type: "api_key", key: "fixture-key" } }); - expect(await discover(lookup(env))).toEqual([]); + expect(await logins(lookup(env))).toEqual([]); await writeFile(path, "broken"); - expect(await discover(lookup(env))).toEqual([]); + expect(await logins(lookup(env))).toEqual([]); }); test("expired Pi tokens are unavailable without calling usage or refreshing", async () => { await json(join(home, ".pi", "agent", "auth.json"), { anthropic: { type: "oauth", access: "fixture-expired", accountId: "pi-account", expires: 500 }, }); - const [input] = await discover(lookup()); + const [input] = await logins(lookup()); expect( ( await fetchUsage( @@ -129,7 +142,7 @@ test("expired Pi tokens are unavailable without calling usage or refreshing", as }); for (const location of ["default", "profile", "xdg", "override", "config"]) { - test(`OMP discovers enabled live OAuth rows in ${location} store read-only`, async () => { + test(`OMP discovers enabled OAuth rows including expired logins in ${location} store read-only`, async () => { let path = join(home, ".omp", "agent", "agent.db"); let env: NodeJS.ProcessEnv = { OMP_AUTH_BROKER_URL: "https://broker.test" }; if (location === "profile") { @@ -151,9 +164,12 @@ for (const location of ["default", "profile", "xdg", "override", "config"]) { } await database(path); const before = await readFile(path); - const inputs = await discover(lookup(env)); - expect(inputs).toEqual([{ route: { store: "omp", path, credentialId: 1 } }]); - expect(await account(inputs[0]!, env)).toEqual({ key: "pi-account.pi-org" }); + const inputs = await logins(lookup(env)); + expect(inputs).toEqual([ + { route: { store: "omp", path, credentialId: 1 } }, + { route: { store: "omp", path, credentialId: 2 } }, + ]); + expect(await accountIdentity(inputs[0]!, env)).toEqual({ key: "pi-account.pi-org" }); expect( ( await fetchUsage( @@ -175,56 +191,52 @@ test("missing SQLite skips only OMP", async () => { await json(join(home, ".pi", "agent", "auth.json"), { anthropic: { type: "oauth", access: "fixture-pi" }, }); - const inputs = await discover({ ...lookup(), sqlite: () => undefined }); + const inputs = await logins({ ...lookup(), sqlite: () => undefined }); expect(inputs).toEqual([ { route: { store: "pi", path: join(home, ".pi", "agent", "auth.json") } }, ]); }); -test("Claude file and keychain are independent discovery candidates", async () => { - const path = join(home, ".claude", ".credentials.json"); - await json(path, { claudeAiOauth: { accessToken: "fixture-file" } }); - const inputs = await discover({ +test("Claude Keychain login replaces the fallback file", async () => { + await json(join(home, ".claude", ".credentials.json"), { + claudeAiOauth: { accessToken: "fixture-file", expiresAt: 500 }, + }); + const inputs = await logins({ ...lookup(), platform: "darwin", readKeychainCredentials: async () => ({ claudeAiOauth: { accessToken: "fixture-keychain" } }), }); - expect(inputs).toEqual([{ route: { store: "claude", path } }, { route: { store: "keychain" } }]); + expect(inputs).toEqual([{ route: { store: "keychain" } }]); }); test("Claude account metadata alone does not discover a login", async () => { await json(join(home, ".claude.json"), { oauthAccount: { accountUuid: "old", organizationUuid: "old-org" }, }); - expect(await discover(lookup())).toEqual([]); - expect( - await identify( - { route: { store: "claude", path: join(home, ".claude", ".credentials.json") } }, - profile, - Date.now, - lookup(), - ), - ).toBeNull(); + expect(await logins(lookup())).toEqual([]); }); test("keychain identity belongs to its token even when Claude Code metadata names another account", async () => { await json(join(home, ".claude.json"), { oauthAccount: { accountUuid: "stale", organizationUuid: "stale-org" }, }); - const identity = await identify({ route: { store: "keychain" } }, profile, () => 1000, { - ...lookup(), - platform: "darwin", - readKeychainCredentials: async () => ({ - claudeAiOauth: { accessToken: "fixture-keychain-other" }, - }), - }); - expect(identity).toEqual({ key: "pi-account.pi-org" }); + const [identity] = await discover( + { + ...lookup(), + platform: "darwin", + readKeychainCredentials: async () => ({ + claudeAiOauth: { accessToken: "fixture-keychain-other" }, + }), + }, + profile, + ); + expect(identity).toEqual({ key: "pi-account.pi-org", input: { route: { store: "keychain" } } }); }); test("OMP fetch re-reads its row after rotation and skips a newly disabled login", async () => { const path = join(home, ".omp", "agent", "agent.db"); await database(path); - const [input] = await discover(lookup()); + const [input] = await logins(lookup()); const { DatabaseSync } = await import("node:sqlite"); const db = new DatabaseSync(path); db.prepare("UPDATE auth_credentials SET data = ? WHERE id = 1").run( @@ -246,17 +258,15 @@ test("OMP fetch re-reads its row after rotation and skips a newly disabled login ).toBe("unavailable"); db.prepare("UPDATE auth_credentials SET disabled_cause = 'revoked' WHERE id = 1").run(); db.close(); - expect( - ( - await fetchUsage( - input!, - async () => { - throw new Error("disabled login must not fetch"); - }, - lookup(), - ) - ).status, - ).toBe("unavailable"); + await expect( + fetchUsage( + input!, + async () => { + throw new Error("must not fetch"); + }, + lookup(), + ), + ).rejects.toThrow("login store no longer exists"); }); test("Claude discovery honors CLAUDE_CONFIG_DIR", async () => { @@ -268,7 +278,7 @@ test("Claude discovery honors CLAUDE_CONFIG_DIR", async () => { await json(join(ignoredDirectory, ".credentials.json"), { claudeAiOauth: { accessToken: "fixture-other" }, }); - const inputs = await discover( + const inputs = await logins( lookup({ CLAUDE_CONFIG_DIR: directory, CLAUDE_HOME: ignoredDirectory }), ); expect(inputs).toEqual([ diff --git a/plugins/claude-usage-source/server/stores.ts b/plugins/claude-usage-source/server/stores.ts index 0c41c1478c..5da17a1e51 100644 --- a/plugins/claude-usage-source/server/stores.ts +++ b/plugins/claude-usage-source/server/stores.ts @@ -97,7 +97,6 @@ function ompRows(path: string, lookup: StoreLookup) { if (!row.success) return []; try { const oauth = oauthSchema.parse(JSON.parse(row.data.data)); - if (oauth.expires === undefined || oauth.expires <= (lookup.now ?? Date.now)()) return []; return [{ id: row.data.id, oauth }]; } catch { return []; diff --git a/plugins/claude-usage-source/server/usage.test.ts b/plugins/claude-usage-source/server/usage.test.ts index f282a25ae9..289076a755 100644 --- a/plugins/claude-usage-source/server/usage.test.ts +++ b/plugins/claude-usage-source/server/usage.test.ts @@ -2,9 +2,9 @@ import { readFileSync, writeFileSync, mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { fetchUsage } from "./usage.js"; +import { fetchUsage, discover } from "./usage.js"; import { inputSchema, type UsageInput } from "../shared/input.js"; -import type { UsageReport } from "@getpaseo/plugin/server/usage"; +import { hashAccountKey, type UsageReport } from "@getpaseo/plugin/server/usage"; function credentialInput(directory: string): UsageInput { return { route: { store: "claude", path: join(directory, ".credentials.json") } }; @@ -189,15 +189,13 @@ describe("claude usage source", () => { }); }); - it("returns unavailable Claude usage when credentials are missing", async () => { - fetchApi = vi.fn() as never; - - const result = await service().listUsage(); - const claude = findProvider(result, "claude"); - - expect(claude.status).toBe("unavailable"); - expect(fetchApi).not.toHaveBeenCalled(); - }); + it.each(["empty home", "unrelated files"])( + "discovers no Claude logins in %s", + async (scenario) => { + if (scenario === "unrelated files") writeFileSync(join(homeDir, "unrelated.json"), "{}"); + expect(await discover({ home: homeDir, env: {}, platform: "linux" })).toEqual([]); + }, + ); it("returns unavailable on 401 without refreshing or rewriting credentials", async () => { writeClaudeCredentials(claudeHome, "at_expired", "rt_valid"); @@ -639,9 +637,8 @@ describe("Claude usage source scoped limit reconciliation", () => { }); }); -it("identify uses the credential token profile instead of stale Claude config", async () => { +it("discovery uses the credential token profile instead of stale Claude config", async () => { const { mkdtemp, writeFile, rm } = await import("node:fs/promises"); - const { identify } = await import("./usage.js"); const directory = await mkdtemp(join(tmpdir(), "claude-identity-")); try { await writeFile( @@ -656,7 +653,7 @@ it("identify uses the credential token profile instead of stale Claude config", ); writeClaudeCredentials(directory, "fixture-local-account"); expect( - await identify( + await accountFor( credentialInput(directory), async () => jsonResponse({ @@ -676,7 +673,6 @@ it("identify uses the credential token profile instead of stale Claude config", }); it("Claude credentials without account metadata use a cached OAuth profile identity", async () => { - const { identify } = await import("./usage.js"); let calls = 0; const fetchProfile: typeof fetch = async () => { calls++; @@ -690,13 +686,13 @@ it("Claude credentials without account metadata use a cached OAuth profile ident }; const lookup = tokenLookup("fixture-claude-token"); expect( - await identify(credentialInput(lookup.claudeHome), fetchProfile, Date.now, lookup), + await accountFor(credentialInput(lookup.claudeHome), fetchProfile, Date.now, lookup), ).toEqual({ key: "account-uuid.org-uuid", label: "test@example.com", }); expect( - await identify(credentialInput(lookup.claudeHome), fetchProfile, Date.now, lookup), + await accountFor(credentialInput(lookup.claudeHome), fetchProfile, Date.now, lookup), ).toEqual({ key: "account-uuid.org-uuid", label: "test@example.com", @@ -705,7 +701,6 @@ it("Claude credentials without account metadata use a cached OAuth profile ident }); it("identifies the same credential route that fetch uses when oauthAccount is absent", async () => { - const { identify } = await import("./usage.js"); const directory = mkdtempSync(join(tmpdir(), "claude-credential-identity-")); try { writeClaudeCredentials(directory, "fixture-credential-token"); @@ -721,7 +716,7 @@ it("identifies the same credential route that fetch uses when oauthAccount is ab ); expect(usageRequested).toBe(true); let profileRequested = false; - const account = await identify( + const account = await accountFor( credentialInput(directory), async () => { profileRequested = true; @@ -741,7 +736,6 @@ it("identifies the same credential route that fetch uses when oauthAccount is ab }); it("expires cached token profiles after five minutes", async () => { - const { identify } = await import("./usage.js"); let now = 0; let calls = 0; const fetchProfile: typeof fetch = async () => { @@ -750,16 +744,15 @@ it("expires cached token profiles after five minutes", async () => { }; const lookup = tokenLookup("ttl-fixture-token"); const input = credentialInput(lookup.claudeHome); - await identify(input, fetchProfile, () => now, lookup); - await identify(input, fetchProfile, () => now, lookup); + await accountFor(input, fetchProfile, () => now, lookup); + await accountFor(input, fetchProfile, () => now, lookup); expect(calls).toBe(1); now = 300_001; - await identify(input, fetchProfile, () => now, lookup); + await accountFor(input, fetchProfile, () => now, lookup); expect(calls).toBe(2); }); -it("identify and fetch share the explicit macOS keychain route", async () => { - const { identify } = await import("./usage.js"); +it("discovery and fetch share the explicit macOS keychain route", async () => { const directory = mkdtempSync(join(tmpdir(), "claude-keychain-identity-")); try { const lookup = { @@ -780,7 +773,7 @@ it("identify and fetch share the explicit macOS keychain route", async () => { lookup, ); expect(usageRequested).toBe(true); - const account = await identify( + const account = await accountFor( { route: { store: "keychain" } }, async () => jsonResponse({ @@ -796,8 +789,7 @@ it("identify and fetch share the explicit macOS keychain route", async () => { } }); -it("identify and fetch share the explicit Claude credential route", async () => { - const { identify } = await import("./usage.js"); +it("discovery and fetch share the explicit Claude credential route", async () => { const directory = mkdtempSync(join(tmpdir(), "claude-home-identity-")); try { writeClaudeCredentials(directory, "home-fixture-token"); @@ -812,7 +804,7 @@ it("identify and fetch share the explicit Claude credential route", async () => lookup, ); expect(usageRequested).toBe(true); - const identity = await identify( + const identity = await accountFor( credentialInput(directory), async () => jsonResponse({ @@ -829,7 +821,6 @@ it("identify and fetch share the explicit Claude credential route", async () => }); it("bounds cached profiles across many token rotations", async () => { - const { identify } = await import("./usage.js"); let calls = 0; const fetchProfile: typeof fetch = async () => { calls++; @@ -837,10 +828,10 @@ it("bounds cached profiles across many token rotations", async () => { }; for (let index = 0; index < 129; index++) { const lookup = tokenLookup(`rotation-fixture-${index}`); - await identify(credentialInput(lookup.claudeHome), fetchProfile, Date.now, lookup); + await accountFor(credentialInput(lookup.claudeHome), fetchProfile, Date.now, lookup); } const lookup = tokenLookup("rotation-fixture-0"); - await identify(credentialInput(lookup.claudeHome), fetchProfile, Date.now, lookup); + await accountFor(credentialInput(lookup.claudeHome), fetchProfile, Date.now, lookup); expect(calls).toBe(130); }); @@ -863,7 +854,6 @@ function tokenLookup(accessToken: string) { } it("discovers a Pi OAuth login independently of the CLI login", async () => { - const { discover } = await import("./usage.js"); const { mkdir, mkdtemp, writeFile, rm } = await import("node:fs/promises"); const directory = await mkdtemp(join(tmpdir(), "usage-pi-discovery-")); try { @@ -875,10 +865,137 @@ it("discovers a Pi OAuth login independently of the CLI login", async () => { }), ); const inputs = await discover({ home: directory, env: {}, platform: "linux" }); - expect(inputs).toContainEqual({ + expect(inputs.map((account) => account.input)).toContainEqual({ route: { store: "pi", path: join(directory, ".pi", "agent", "auth.json") }, }); } finally { await rm(directory, { recursive: true, force: true }); } }); + +it("keeps an expired Keychain login without calling the network and prefers Keychain over the fallback file", async () => { + const directory = mkdtempSync(join(tmpdir(), "claude-expired-")); + try { + writeClaudeCredentials(directory, "stale-file"); + const lookup = { + home: directory, + claudeHome: directory, + env: {}, + platform: "darwin" as const, + now: () => 10_800_000, + readKeychainCredentials: async () => ({ + claudeAiOauth: { accessToken: "expired-keychain", expiresAt: 0 }, + }), + }; + let calls = 0; + const network: typeof fetch = async () => { + calls++; + throw new Error("must not call network"); + }; + const accounts = await discover(lookup, network); + expect(accounts).toEqual([ + { key: expect.stringMatching(/^[a-f0-9]{64}$/), input: { route: { store: "keychain" } } }, + ]); + expect(await fetchUsage(accounts[0]!.input as UsageInput, network, lookup)).toEqual({ + status: "unavailable", + problem: { kind: "expired", expiresAt: "1970-01-01T00:00:00.000Z", refreshedBy: "claude" }, + }); + expect(calls).toBe(0); + } finally { + rmSync(directory, { recursive: true, force: true }); + } +}); + +async function accountFor( + input: UsageInput, + fetchApi: typeof fetch, + now: () => number, + lookup: Parameters[0] = {}, +) { + const accounts = await discover({ ...lookup, now }, fetchApi); + const account = accounts.find( + (candidate) => JSON.stringify(candidate.input) === JSON.stringify(input), + ); + if (!account) throw new Error("Expected discovered login"); + return { key: account.key, ...(account.label ? { label: account.label } : {}) }; +} +it.each([401, 403])("fetch reports rejected usage HTTP %i", async (status) => { + const lookup = tokenLookup(`usage-rejection-${status}`); + expect( + await fetchUsage( + credentialInput(lookup.claudeHome), + async () => new Response(null, { status }), + lookup, + ), + ).toEqual({ + status: "unavailable", + problem: { kind: "rejected", status, refreshedBy: "claude" }, + }); +}); + +it.each([401, 403])("a rejected profile HTTP %i keeps a locator-keyed account", async (status) => { + const lookup = tokenLookup(`profile-rejection-${status}`); + const api: typeof fetch = async () => new Response(null, { status }); + const accounts = await discover(lookup, api); + expect(accounts).toEqual([ + { key: expect.stringMatching(/^[a-f0-9]{64}$/), input: credentialInput(lookup.claudeHome) }, + ]); + let fetches = 0; + expect( + await fetchUsage( + credentialInput(lookup.claudeHome), + async () => { + fetches++; + throw new Error("profile already rejected"); + }, + lookup, + ), + ).toEqual({ + status: "unavailable", + problem: { kind: "rejected", status, refreshedBy: "claude" }, + }); + expect(fetches).toBe(0); +}); + +it.each([ + { failure: "HTTP 500", networkError: false, error: "Claude usage API returned 500" }, + { failure: "a network error", networkError: true, error: "Network unavailable" }, +])( + "keeps a locator-keyed login when the profile fails with $failure", + async ({ networkError, error }) => { + const lookup = tokenLookup(`profile-failure-${networkError}`); + const input = credentialInput(lookup.claudeHome); + const api: typeof fetch = async () => { + if (networkError) throw new Error(error); + return new Response(null, { status: 500 }); + }; + const accounts = await discover(lookup, api); + expect(accounts).toEqual([{ key: hashAccountKey(JSON.stringify(input.route)), input }]); + await expect(fetchUsage(accounts[0]!.input as UsageInput, api, lookup)).rejects.toThrow(error); + }, +); + +it("re-reads a refreshed login at the original locator", async () => { + const lookup = tokenLookup("expiring-token"); + writeFileSync( + join(lookup.claudeHome, ".credentials.json"), + JSON.stringify({ claudeAiOauth: { accessToken: "expiring-token", expiresAt: 0 } }), + ); + const [account] = await discover(lookup, async () => { + throw new Error("expired token must not fetch"); + }); + expect(await fetchUsage(account!.input as UsageInput, fetch, lookup)).toEqual({ + status: "unavailable", + problem: { kind: "expired", expiresAt: "1970-01-01T00:00:00.000Z", refreshedBy: "claude" }, + }); + writeClaudeCredentials(lookup.claudeHome, "rotated-token"); + const report = await fetchUsage( + account!.input as UsageInput, + async (_url, init) => { + expect(new Headers(init?.headers).get("Authorization")).toBe("Bearer rotated-token"); + return jsonResponse(makeClaudeResponse()); + }, + lookup, + ); + expect(report.status).toBe("available"); +}); diff --git a/plugins/claude-usage-source/server/usage.ts b/plugins/claude-usage-source/server/usage.ts index 34464d8b53..03509bba21 100644 --- a/plugins/claude-usage-source/server/usage.ts +++ b/plugins/claude-usage-source/server/usage.ts @@ -9,7 +9,8 @@ import { z } from "zod"; import { hashAccountKey, toneFromUsedPct, - unavailableUsage, + unavailable, + type UsageAccount, windowFromUsedPct, type UsageReport, type UsageWindow, @@ -27,6 +28,7 @@ const ClaudeCredentialsSchema = z.object({ claudeAiOauth: z .object({ accessToken: z.string().optional(), + expiresAt: z.number().optional(), refreshToken: z.string().optional(), subscriptionType: z.string().optional(), rateLimitTier: z.string().optional(), @@ -356,20 +358,41 @@ async function keychainCredentialRecord( return parsed.success ? toCredentialRecord(parsed.data) : null; } -export async function discover(lookup: ClaudeCredentialLookup = {}): Promise { +export async function discover( + lookup: ClaudeCredentialLookup = {}, + fetchApi: typeof fetch = fetch, +): Promise { + // Claude Code only reads the file when its Keychain login is empty. + const primary: UsageInput = (await keychainCredentialRecord(lookup)) + ? { route: { store: "keychain" } } + : { route: { store: "claude", path: claudeCredentialPath(lookup) } }; const candidates: UsageInput[] = [ - { route: { store: "claude", path: claudeCredentialPath(lookup) } }, - ]; - if ((lookup.platform ?? process.platform) === "darwin") - candidates.push({ route: { store: "keychain" } }); - candidates.push( + primary, { route: { store: "pi", path: piAuthPath(lookup) } }, ...discoverOmp(lookup).map((route) => ({ route })), - ); - const present: UsageInput[] = []; - for (const input of candidates) - if (await resolveClaudeCredentials(input, lookup)) present.push(input); - return present; + ]; + const accounts: UsageAccount[] = []; + for (const input of candidates) { + const credentials = await resolveClaudeCredentials(input, lookup); + if (!credentials) continue; + const fallback = { key: hashAccountKey(JSON.stringify(input.route)), input }; + if (credentials.expires !== undefined && credentials.expires <= (lookup.now ?? Date.now)()) { + accounts.push(fallback); + continue; + } + try { + const profile = await readProfile( + credentials.oauth.accessToken, + fetchApi, + lookup.now ?? Date.now, + ); + accounts.push("status" in profile ? fallback : { ...profile, input }); + } catch { + // Keep the login visible; fetching usage reports the vendor failure. + accounts.push(fallback); + } + } + return accounts; } /** Re-read the selected login; the harness owns token refresh. */ @@ -397,7 +420,9 @@ async function readCredentialFile(path: string): Promise { + async function callClaudeApi(token: string): Promise { const res = await fetchApi("https://api.anthropic.com/api/oauth/usage", { signal: AbortSignal.timeout(15_000), headers: { @@ -442,28 +467,38 @@ export async function fetchUsage( "anthropic-beta": CLAUDE_OAUTH_BETA, }, }); - if (res.status === 401 || res.status === 403) return "NEEDS_AUTH"; + if (res.status === 401 || res.status === 403) return res.status; if (!res.ok) throw new Error(`Claude usage API returned ${res.status}`); return ClaudeUsageResponseSchema.parse(await res.json()); } const credentials = await resolveClaudeCredentials(input, credentialLookup); + if (!credentials) throw new Error("Claude login store no longer exists"); + const refreshedBy = + input.route.store === "claude" || input.route.store === "keychain" + ? "claude" + : input.route.store; if ( - !credentials || - (credentials.expires !== undefined && - credentials.expires <= (credentialLookup.now ?? Date.now)()) - ) { - return unavailableUsage(); - } + credentials.expires !== undefined && + credentials.expires <= (credentialLookup.now ?? Date.now)() + ) + return unavailable({ + kind: "expired", + expiresAt: new Date(credentials.expires).toISOString(), + refreshedBy, + }); + const profile = await cachedProfile( + credentials.oauth.accessToken, + (credentialLookup.now ?? Date.now)(), + ); + if (profile && "status" in profile) + return unavailable({ kind: "rejected", status: profile.status, refreshedBy }); const { oauth } = credentials; const plan = buildClaudePlan(oauth.subscriptionType, oauth.rateLimitTier); const resp = await callClaudeApi(oauth.accessToken); - if (resp === "NEEDS_AUTH") { - // Read-only on credentials; the Claude CLI owns refresh. See docs/providers.md. - return unavailableUsage(); - } + if (typeof resp === "number") return unavailable({ kind: "rejected", status: resp, refreshedBy }); const scoped = reconcileScopedLimits( legacyScopedLimits(resp), @@ -498,26 +533,27 @@ export async function fetchUsage( } // The OAuth usage endpoint meters the active organization selected by the token. -type ClaudeIdentity = { key: string; label?: string } | null; +type ClaudeIdentity = { key: string; label?: string } | { status: number }; const PROFILE_TTL_MS = 300_000; const PROFILE_CACHE_LIMIT = 128; const profileCache = new Map }>(); +function cachedProfile(token: string, now: number): Promise | undefined { + pruneProfileCache(now); + return profileCache.get(hashAccountKey(token))?.result; +} + function pruneProfileCache(now: number): void { for (const [key, entry] of profileCache) { if (now - entry.at >= PROFILE_TTL_MS) profileCache.delete(key); } } -export async function identify( - input: UsageInput, - fetchApi: typeof fetch = fetch, - now: () => number = Date.now, - credentialLookup: ClaudeCredentialLookup = {}, -) { - const credentials = await resolveClaudeCredentials(input, credentialLookup); - if (!credentials) return null; - const token = credentials.oauth.accessToken; +async function readProfile( + token: string, + fetchApi: typeof fetch, + now: () => number, +): Promise { const tokenHash = hashAccountKey(token); pruneProfileCache(now()); const pending = profileCache.get(tokenHash); @@ -536,7 +572,7 @@ export async function identify( "anthropic-beta": CLAUDE_OAUTH_BETA, }, }); - if (response.status === 401 || response.status === 403) return null; + if (response.status === 401 || response.status === 403) return { status: response.status }; if (!response.ok) throw new Error(`Claude profile API returned ${response.status}`); const profile = z .object({ diff --git a/plugins/codex-usage-source/index.server.ts b/plugins/codex-usage-source/index.server.ts index 665860aa5a..786bfe5486 100644 --- a/plugins/codex-usage-source/index.server.ts +++ b/plugins/codex-usage-source/index.server.ts @@ -1,6 +1,6 @@ import type { PluginServerContext } from "@getpaseo/plugin/server"; import { inputSchema } from "./shared/input.js"; -import { discover, fetchUsage, identify } from "./server/usage.js"; +import { discover, fetchUsage } from "./server/usage.js"; export default function contribute(server: PluginServerContext) { server.registerUsageSource({ @@ -9,7 +9,6 @@ export default function contribute(server: PluginServerContext) { icon: "icon.svg", input: inputSchema, discover: () => discover(), - identify, fetch: fetchUsage, }); return () => {}; diff --git a/plugins/codex-usage-source/server/stores.test.ts b/plugins/codex-usage-source/server/stores.test.ts index 191c92d25f..3368a95147 100644 --- a/plugins/codex-usage-source/server/stores.test.ts +++ b/plugins/codex-usage-source/server/stores.test.ts @@ -2,7 +2,7 @@ import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, beforeEach, expect, test } from "vitest"; -import { discover, fetchUsage, identify } from "./usage.js"; +import { discover, fetchUsage } from "./usage.js"; let home: string; beforeEach(async () => { @@ -19,8 +19,21 @@ async function json(path: string, value: unknown) { await mkdir(dirname(path), { recursive: true }); await writeFile(path, JSON.stringify(value)); } -async function account(input: Parameters[0], env: NodeJS.ProcessEnv = {}) { - return identify(input, lookup(env)); +async function accountIdentity( + input: Parameters[0], + env: NodeJS.ProcessEnv = {}, +) { + const accounts = await discover(lookup(env)); + const account = accounts.find( + (candidate) => JSON.stringify(candidate.input) === JSON.stringify(input), + ); + if (!account) throw new Error("Expected discovered login"); + return { key: account.key, ...(account.label ? { label: account.label } : {}) }; +} +async function logins(options: Parameters[0]) { + return (await discover(options)).map( + (account) => account.input as Parameters[0], + ); } async function database(path: string) { await mkdir((await import("node:path")).dirname(path), { recursive: true }); @@ -70,9 +83,9 @@ test("Pi uses its own OAuth identity and re-reads rotated tokens without writing await json(join(home, ".claude.json"), { oauthAccount: { accountUuid: "other", organizationUuid: "other-org" }, }); - const inputs = await discover(lookup()); + const inputs = await logins(lookup()); expect(inputs).toHaveLength(1); - expect(await account(inputs[0]!)).toEqual({ key: "pi-account" }); + expect(await accountIdentity(inputs[0]!)).toEqual({ key: "pi-account" }); await json(path, { "openai-codex": { type: "oauth", access: "fixture-rotated", accountId: "pi-account" }, }); @@ -99,11 +112,11 @@ test("Pi honors directory override and ignores API keys and malformed files", as await json(path, { "openai-codex": { type: "oauth", access: "fixture-pi", accountId: "pi-account" }, }); - expect(await discover(lookup(env))).toEqual([{ route: { store: "pi", path } }]); + expect(await logins(lookup(env))).toEqual([{ route: { store: "pi", path } }]); await json(path, { "openai-codex": { type: "api_key", key: "fixture-key" } }); - expect(await discover(lookup(env))).toEqual([]); + expect(await logins(lookup(env))).toEqual([]); await writeFile(path, "broken"); - expect(await discover(lookup(env))).toEqual([]); + expect(await logins(lookup(env))).toEqual([]); }); test("expired Pi tokens are unavailable without calling usage or refreshing", async () => { @@ -115,7 +128,7 @@ test("expired Pi tokens are unavailable without calling usage or refreshing", as expires: 500, }, }); - const [input] = await discover(lookup()); + const [input] = await logins(lookup()); expect( ( await fetchUsage( @@ -152,9 +165,12 @@ for (const location of ["default", "profile", "xdg", "override", "config"]) { } await database(path); const before = await readFile(path); - const inputs = await discover(lookup(env)); - expect(inputs).toEqual([{ route: { store: "omp", path, credentialId: 1 } }]); - expect(await account(inputs[0]!, env)).toEqual({ key: "omp-account" }); + const inputs = await logins(lookup(env)); + expect(inputs).toEqual([ + { route: { store: "omp", path, credentialId: 1 } }, + { route: { store: "omp", path, credentialId: 2 } }, + ]); + expect(await accountIdentity(inputs[0]!, env)).toEqual({ key: "omp-account" }); expect( ( await fetchUsage( @@ -176,7 +192,7 @@ test("missing SQLite skips only OMP", async () => { await json(join(home, ".pi", "agent", "auth.json"), { "openai-codex": { type: "oauth", access: "fixture-pi" }, }); - const inputs = await discover({ ...lookup(), sqlite: () => undefined }); + const inputs = await logins({ ...lookup(), sqlite: () => undefined }); expect(inputs).toEqual([ { route: { store: "pi", path: join(home, ".pi", "agent", "auth.json") } }, ]); @@ -199,17 +215,17 @@ test("Codex and OpenCode discover accounts in preference order with XDG and CODE await json(join(data, "opencode", "auth.json"), { openai: { type: "oauth", access: "fixture-open", accountId: "same" }, }); - const inputs = await discover(lookup(env)); + const inputs = await logins(lookup(env)); expect(inputs.map((input) => input.route.store)).toEqual(["codex", "codex", "opencode"]); - for (const input of inputs) expect(await account(input, env)).toEqual({ key: "same" }); + for (const input of inputs) expect(await accountIdentity(input, env)).toEqual({ key: "same" }); await json(join(data, "opencode", "auth.json"), { openai: { type: "api", key: "fixture-key" } }); - expect(await discover(lookup(env))).toHaveLength(2); + expect(await logins(lookup(env))).toHaveLength(2); }); test("OMP fetch re-reads its row after rotation and skips a newly disabled login", async () => { const path = join(home, ".omp", "agent", "agent.db"); await database(path); - const [input] = await discover(lookup()); + const [input] = await logins(lookup()); const { DatabaseSync } = await import("node:sqlite"); const db = new DatabaseSync(path); db.prepare("UPDATE auth_credentials SET data = ? WHERE id = 1").run( @@ -231,15 +247,13 @@ test("OMP fetch re-reads its row after rotation and skips a newly disabled login ).toBe("unavailable"); db.prepare("UPDATE auth_credentials SET disabled_cause = 'revoked' WHERE id = 1").run(); db.close(); - expect( - ( - await fetchUsage( - input!, - async () => { - throw new Error("disabled login must not fetch"); - }, - lookup(), - ) - ).status, - ).toBe("unavailable"); + await expect( + fetchUsage( + input!, + async () => { + throw new Error("must not fetch"); + }, + lookup(), + ), + ).rejects.toThrow("login store no longer exists"); }); diff --git a/plugins/codex-usage-source/server/stores.ts b/plugins/codex-usage-source/server/stores.ts index 2f20beb24e..7f997d7841 100644 --- a/plugins/codex-usage-source/server/stores.ts +++ b/plugins/codex-usage-source/server/stores.ts @@ -97,7 +97,6 @@ function ompRows(path: string, lookup: StoreLookup) { if (!row.success) return []; try { const oauth = oauthSchema.parse(JSON.parse(row.data.data)); - if (oauth.expires === undefined || oauth.expires <= (lookup.now ?? Date.now)()) return []; return [{ id: row.data.id, oauth }]; } catch { return []; diff --git a/plugins/codex-usage-source/server/usage.test.ts b/plugins/codex-usage-source/server/usage.test.ts index 27aab6e6ac..c70066c886 100644 --- a/plugins/codex-usage-source/server/usage.test.ts +++ b/plugins/codex-usage-source/server/usage.test.ts @@ -2,7 +2,7 @@ import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, beforeEach, expect, test } from "vitest"; -import { fetchUsage, identify } from "./usage.js"; +import { fetchUsage, discover } from "./usage.js"; import { inputSchema, type CodexUsageInput } from "../shared/input.js"; @@ -80,12 +80,12 @@ test("discovery input requires a route and rejects raw credentials", () => { expect(() => inputSchema.parse({})).toThrow(); }); -test("identify returns a key when fetch finds Codex token credentials", async () => { +test("discovery returns a key when fetch finds Codex token credentials", async () => { await writeAuth("fixture-supplied", "account-supplied"); await fetchUsage(authInput(fixtureHome), (_url, init) => response(init?.headers ?? {}, "account-supplied"), ); - expect(await identify(authInput(fixtureHome))).toEqual({ key: "account-supplied" }); + expect(await accountFor(authInput(fixtureHome))).toEqual({ key: "account-supplied" }); }); test("coerces credit balance and marks a 96 percent window dangerous", async () => { @@ -136,12 +136,10 @@ test("summarizes the session and weekly windows by default, not code review", as ]); }); -test("HTML usage body is unavailable", async () => { - const report = await fetchUsage( - authInput(fixtureHome), - async () => new Response("Login", { status: 200 }), - ); - expect(report.status).toBe("unavailable"); +test("HTML usage body is an error", async () => { + await expect( + fetchUsage(authInput(fixtureHome), async () => new Response("Login")), + ).rejects.toThrow("Codex usage API returned HTML"); }); test("401 leaves auth.json byte for byte unchanged and makes no refresh request", async () => { @@ -178,14 +176,16 @@ test("auth account claim survives token rotation", async () => { const token = (suffix: string) => `header.${Buffer.from(JSON.stringify({ "https://api.openai.com/auth": { chatgpt_account_id: "work-id" }, suffix })).toString("base64url")}.signature`; await writeAuth(token("first")); - expect(await identify(authInput(fixtureHome))).toEqual({ key: "work-id" }); + expect(await accountFor(authInput(fixtureHome))).toEqual({ key: "work-id" }); await writeAuth(token("second")); - expect(await identify(authInput(fixtureHome))).toEqual({ key: "work-id" }); + expect(await accountFor(authInput(fixtureHome))).toEqual({ key: "work-id" }); await writeAuth("opaque-token"); - expect(await identify(authInput(fixtureHome))).toBeNull(); + expect(await accountFor(authInput(fixtureHome))).toEqual({ + key: expect.stringMatching(/^[a-f0-9]{64}$/), + }); }); -test("identify reads an email label from auth id_token when access token has no profile", async () => { +test("discovery reads an email label from auth id_token when access token has no profile", async () => { const home = await mkdtemp(join(tmpdir(), "usage-codex-label-")); try { process.env["CODEX_HOME"] = home; @@ -196,7 +196,7 @@ test("identify reads an email label from auth id_token when access token has no tokens: { account_id: "account-id", access_token: "opaque-token", id_token: idToken }, }), ); - expect(await identify(authInput(home))).toEqual({ + expect(await accountFor(authInput(home))).toEqual({ key: "account-id", label: "id-owner@example.test", }); @@ -206,7 +206,6 @@ test("identify reads an email label from auth id_token when access token has no }); test("discovers a Pi OAuth login independently of the CLI login", async () => { - const { discover } = await import("./usage.js"); const directory = await mkdtemp(join(tmpdir(), "usage-pi-discovery-")); try { await mkdir(join(directory, ".pi", "agent"), { recursive: true }); @@ -217,10 +216,47 @@ test("discovers a Pi OAuth login independently of the CLI login", async () => { }), ); const inputs = await discover({ home: directory, env: {}, platform: "linux" }); - expect(inputs).toContainEqual({ + expect(inputs.map((account) => account.input)).toContainEqual({ route: { store: "pi", path: join(directory, ".pi", "agent", "auth.json") }, }); } finally { await rm(directory, { recursive: true, force: true }); } }); + +async function accountFor(input: CodexUsageInput) { + const accounts = await discover({ + home: fixtureHome, + env: { + CODEX_HOME: + input.route.store === "codex" + ? (await import("node:path")).dirname(input.route.path) + : fixtureHome, + }, + platform: "linux", + }); + const account = accounts.find( + (candidate) => JSON.stringify(candidate.input) === JSON.stringify(input), + ); + if (!account) throw new Error("Expected discovered login"); + return { key: account.key, ...(account.label ? { label: account.label } : {}) }; +} + +test.each(["empty home", "unrelated files"])( + "discovers no Codex accounts in %s", + async (scenario) => { + const home = await mkdtemp(join(tmpdir(), "codex-no-login-")); + try { + if (scenario === "unrelated files") await writeFile(join(home, "unrelated.json"), "{}"); + expect(await discover({ home, env: {}, platform: "linux" })).toEqual([]); + } finally { + await rm(home, { recursive: true, force: true }); + } + }, +); + +test.each([401, 403])("reports a Codex login rejected with HTTP %i", async (status) => { + expect( + await fetchUsage(authInput(fixtureHome), async () => new Response(null, { status })), + ).toEqual({ status: "unavailable", problem: { kind: "rejected", status, refreshedBy: "codex" } }); +}); diff --git a/plugins/codex-usage-source/server/usage.ts b/plugins/codex-usage-source/server/usage.ts index 1feda550c0..106a77336d 100644 --- a/plugins/codex-usage-source/server/usage.ts +++ b/plugins/codex-usage-source/server/usage.ts @@ -3,6 +3,9 @@ import { homedir } from "node:os"; import { join } from "node:path"; import { balanceToneFromRemaining, + hashAccountKey, + unavailable, + type UsageAccount, toneFromUsedPct, windowFromUsedPct, type UsageReport, @@ -39,7 +42,7 @@ interface Auth { expires?: number; } -export async function discover(lookup: StoreLookup = {}): Promise { +export async function discover(lookup: StoreLookup = {}): Promise { const env = lookup.env ?? process.env; const home = lookup.home ?? homedir(); const paths = [ @@ -59,8 +62,11 @@ export async function discover(lookup: StoreLookup = {}): Promise ({ route })), ); - const present: CodexUsageInput[] = []; - for (const input of candidates) if (await readAuth(input, lookup)) present.push(input); + const present: UsageAccount[] = []; + for (const input of candidates) { + const auth = await readAuth(input, lookup); + if (auth) present.push({ ...accountIdentity(auth, input), input }); + } return present; } @@ -104,8 +110,14 @@ export async function fetchUsage( lookup: StoreLookup = {}, ): Promise { const auth = await readAuth(input, lookup); - if (!auth || (auth.expires !== undefined && auth.expires <= (lookup.now ?? Date.now)())) - return { status: "unavailable", windows: [] }; + if (!auth) throw new Error("Codex login store no longer exists"); + const refreshedBy = input.route.store; + if (auth.expires !== undefined && auth.expires <= (lookup.now ?? Date.now)()) + return unavailable({ + kind: "expired", + expiresAt: new Date(auth.expires).toISOString(), + refreshedBy, + }); const headers: Record = { Authorization: `Bearer ${auth.token}`, Accept: "application/json", @@ -117,10 +129,10 @@ export async function fetchUsage( signal: AbortSignal.timeout(15_000), }); if (response.status === 401 || response.status === 403) - return { status: "unavailable", windows: [] }; + return unavailable({ kind: "rejected", status: response.status, refreshedBy }); if (!response.ok) throw new Error(`Codex usage API returned ${response.status}`); const text = await response.text(); - if (text.trim().startsWith("<")) return { status: "unavailable", windows: [] }; + if (text.trim().startsWith("<")) throw new Error("Codex usage API returned HTML"); const usage = responseSchema.parse(JSON.parse(text)); const windows = [ usageWindow( @@ -181,9 +193,7 @@ function claimString(value: unknown): string | null { return typeof value === "string" && value.length > 0 ? value : null; } -export async function identify(input: CodexUsageInput, lookup: StoreLookup = {}) { - const auth = await readAuth(input, lookup); - if (!auth) return null; +function accountIdentity(auth: Auth, input: CodexUsageInput): { key: string; label?: string } { const access = jwtClaims(auth.token); const id = jwtClaims(auth.idToken); const accessAuth = claimObject(access, "https://api.openai.com/auth"); @@ -193,11 +203,11 @@ export async function identify(input: CodexUsageInput, lookup: StoreLookup = {}) claimString(accessAuth?.["chatgpt_account_id"]) ?? claimString(access?.["chatgpt_account_id"]) ?? claimString(idAuth?.["chatgpt_account_id"]); - if (!key) return null; + const label = claimString(claimObject(access, "https://api.openai.com/profile")?.["email"]) ?? claimString(access?.["email"]) ?? claimString(claimObject(id, "https://api.openai.com/profile")?.["email"]) ?? claimString(id?.["email"]); - return { key, ...(label ? { label } : {}) }; + return { key: key ?? hashAccountKey(JSON.stringify(input.route)), ...(label ? { label } : {}) }; } diff --git a/plugins/copilot-usage-source/index.server.ts b/plugins/copilot-usage-source/index.server.ts index 79da4a578e..4f25223cb4 100644 --- a/plugins/copilot-usage-source/index.server.ts +++ b/plugins/copilot-usage-source/index.server.ts @@ -1,6 +1,6 @@ import type { PluginServerContext } from "@getpaseo/plugin/server"; import { inputSchema } from "./shared/input.js"; -import { fetchUsage, identify } from "./server/usage.js"; +import { fetchUsage, discover } from "./server/usage.js"; export default function contribute(server: PluginServerContext) { server.registerUsageSource({ @@ -8,8 +8,7 @@ export default function contribute(server: PluginServerContext) { label: "GitHub Copilot", icon: "icon.svg", input: inputSchema, - discover: async () => [{}], - identify, + discover: () => discover(), fetch: fetchUsage, }); return () => {}; diff --git a/plugins/copilot-usage-source/server/usage.test.ts b/plugins/copilot-usage-source/server/usage.test.ts index 9e34d83ad7..8333f28828 100644 --- a/plugins/copilot-usage-source/server/usage.test.ts +++ b/plugins/copilot-usage-source/server/usage.test.ts @@ -2,7 +2,7 @@ import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { identify, fetchUsage } from "./usage.js"; +import { discover, fetchUsage } from "./usage.js"; import type { UsageReport } from "@getpaseo/plugin/server/usage"; function mockFetch(handlers: Map Response>): typeof fetch { @@ -65,14 +65,14 @@ describe("copilot usage source", () => { ) { return { listUsage: async () => { - const report = await fetchUsage({}, (url, init) => fetchApi(url, init)); + const report = await fetchFirst((url, init) => fetchApi(url, init)); return { providers: [ { providerId: "copilot", ...report, - error: report.error ?? null, - planLabel: report.planLabel ?? null, + error: report.status === "error" ? report.error : null, + planLabel: report.status === "available" ? (report.planLabel ?? null) : null, }, ], }; @@ -112,17 +112,60 @@ describe("copilot usage source", () => { }); }); -it("identify returns a key when fetch finds copilot credentials", async () => { +it("discovery returns a locator when fetch finds copilot credentials", async () => { const previous = process.env["COPILOT_TOKEN"]; try { process.env["COPILOT_TOKEN"] = "fixture-token"; let requested = false; - await fetchUsage({}, async () => { + await fetchFirst(async () => { requested = true; return new Response(null, { status: 401 }); }); expect(requested).toBe(true); - expect(await identify()).toEqual({ key: "default" }); + expect(await discover()).toEqual([ + { key: "default", input: { store: "env", locator: "COPILOT_TOKEN" } }, + ]); + } finally { + if (previous === undefined) delete process.env["COPILOT_TOKEN"]; + else process.env["COPILOT_TOKEN"] = previous; + } +}); + +describe("account discovery", () => { + it.each(["empty home", "unrelated files"])("returns no accounts for %s", async (scenario) => { + const { mkdtemp, writeFile, rm } = await import("node:fs/promises"); + const directory = await mkdtemp(join(tmpdir(), "usage-empty-")); + const original = { ...process.env }; + try { + for (const key of Object.keys(process.env)) delete process.env[key]; + process.env.HOME = directory; + process.env.USERPROFILE = directory; + if (scenario === "unrelated files") await writeFile(join(directory, "unrelated.json"), "{}"); + expect(await discover()).toEqual([]); + } finally { + for (const key of Object.keys(process.env)) delete process.env[key]; + Object.assign(process.env, original); + await rm(directory, { recursive: true, force: true }); + } + }); +}); + +async function fetchFirst(fetchApi: typeof fetch) { + const accounts = await discover(); + const account = accounts[0]; + if (!account) throw new Error("No configured account"); + return fetchUsage(account.input as Parameters[0], fetchApi); +} + +it.each([401, 403])("reports an existing login rejected with HTTP %i", async (status) => { + const previous = process.env["COPILOT_TOKEN"]; + try { + process.env["COPILOT_TOKEN"] = "fixture-rejected-login"; + const report = await fetchUsage( + { store: "env", locator: "COPILOT_TOKEN" }, + async () => new Response(null, { status }), + ); + expect(report).toEqual({ status: "unavailable", problem: { kind: "rejected", status } }); } finally { if (previous === undefined) delete process.env["COPILOT_TOKEN"]; else process.env["COPILOT_TOKEN"] = previous; diff --git a/plugins/copilot-usage-source/server/usage.ts b/plugins/copilot-usage-source/server/usage.ts index 27a5c84e35..78ae61bc9f 100644 --- a/plugins/copilot-usage-source/server/usage.ts +++ b/plugins/copilot-usage-source/server/usage.ts @@ -1,10 +1,11 @@ import type { UsageInput } from "../shared/input.js"; -import { existsSync, promises as fs } from "node:fs"; +import { promises as fs } from "node:fs"; import { homedir } from "node:os"; import { join } from "node:path"; import { z } from "zod"; import { - unavailableUsage, + unavailable, + type UsageAccount, type UsageReport, type UsageDetail, } from "@getpaseo/plugin/server/usage"; @@ -19,39 +20,36 @@ const CopilotUsageResponseSchema = z.object({ quota_reset_date: ApiOptionalStringSchema, }); -async function readGithubCliToken(): Promise { - const candidates: string[] = []; - if (process.env["APPDATA"]) { - candidates.push(join(process.env["APPDATA"], "GitHub CLI", "hosts.yml")); +async function readToken(input: UsageInput): Promise { + if (input.store === "env") return process.env[input.locator]; + try { + const raw = await fs.readFile(input.locator, "utf8"); + return raw.match(/oauth_token:\s*["']?([a-zA-Z0-9_-]+)["']?/)?.[1]; + } catch { + return undefined; } - candidates.push(join(homedir(), ".config", "gh", "hosts.yml")); +} - for (const path of candidates) { - if (!existsSync(path)) continue; - try { - const raw = await fs.readFile(path, "utf8"); - const match = raw.match(/oauth_token:\s*["']?([a-zA-Z0-9_-]+)["']?/); - if (match?.[1]) return match[1]; - } catch { - continue; - } - } - return null; +export async function discover(): Promise { + const candidates: UsageInput[] = ["COPILOT_TOKEN", "GITHUB_TOKEN", "GITHUB_PAT"].map( + (locator) => ({ store: "env", locator }), + ); + if (process.env.APPDATA) + candidates.push({ + store: "file", + locator: join(process.env.APPDATA, "GitHub CLI", "hosts.yml"), + }); + candidates.push({ store: "file", locator: join(homedir(), ".config", "gh", "hosts.yml") }); + for (const input of candidates) if (await readToken(input)) return [{ key: "default", input }]; + return []; } export async function fetchUsage( input: UsageInput, fetchApi: typeof fetch = fetch, ): Promise { - void input; - - const token = - process.env["COPILOT_TOKEN"] || - process.env["GITHUB_TOKEN"] || - process.env["GITHUB_PAT"] || - (await readGithubCliToken()); - - if (!token) return unavailableUsage(); + const token = await readToken(input); + if (!token) throw new Error("Copilot login store no longer exists"); const res = await fetchApi("https://api.github.com/copilot_internal/user", { signal: AbortSignal.timeout(15_000), @@ -65,9 +63,9 @@ export async function fetchUsage( }, }); - if (!res.ok) { - return unavailableUsage(); - } + if (res.status === 401 || res.status === 403) + return unavailable({ kind: "rejected", status: res.status }); + if (!res.ok) throw new Error(`Copilot usage API returned ${res.status}`); const resp = CopilotUsageResponseSchema.parse(await res.json()); const details: UsageDetail[] = resp.quota_reset_date @@ -82,12 +80,3 @@ export async function fetchUsage( details, }; } - -export async function identify() { - const token = - process.env["COPILOT_TOKEN"] || - process.env["GITHUB_TOKEN"] || - process.env["GITHUB_PAT"] || - (await readGithubCliToken()); - return token ? { key: "default" } : null; -} diff --git a/plugins/copilot-usage-source/shared/input.ts b/plugins/copilot-usage-source/shared/input.ts index 9ee4593341..4be5cce316 100644 --- a/plugins/copilot-usage-source/shared/input.ts +++ b/plugins/copilot-usage-source/shared/input.ts @@ -1,4 +1,8 @@ import { z } from "zod"; -export const inputSchema = z.object({}).strict(); - +export const inputSchema = z + .object({ + store: z.enum(["env", "file"]), + locator: z.string().min(1), + }) + .strict(); export type UsageInput = z.infer; diff --git a/plugins/cursor-usage-source/index.server.ts b/plugins/cursor-usage-source/index.server.ts index a4c1947cb4..078c321590 100644 --- a/plugins/cursor-usage-source/index.server.ts +++ b/plugins/cursor-usage-source/index.server.ts @@ -1,6 +1,6 @@ import type { PluginServerContext } from "@getpaseo/plugin/server"; import { inputSchema } from "./shared/input.js"; -import { fetchUsage, identify } from "./server/usage.js"; +import { fetchUsage, discover } from "./server/usage.js"; export default function contribute(server: PluginServerContext) { server.registerUsageSource({ @@ -8,8 +8,7 @@ export default function contribute(server: PluginServerContext) { label: "Cursor", icon: "icon.svg", input: inputSchema, - discover: async () => [{}], - identify, + discover: () => discover(), fetch: fetchUsage, }); return () => {}; diff --git a/plugins/cursor-usage-source/server/usage.test.ts b/plugins/cursor-usage-source/server/usage.test.ts index acd841c14f..21cca3b192 100644 --- a/plugins/cursor-usage-source/server/usage.test.ts +++ b/plugins/cursor-usage-source/server/usage.test.ts @@ -3,7 +3,7 @@ import { createRequire } from "node:module"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { identify, fetchUsage } from "./usage.js"; +import { discover, fetchUsage } from "./usage.js"; import type { UsageReport } from "@getpaseo/plugin/server/usage"; // node:sqlite has no @types/node@20 typings; require it with a narrow local type. @@ -97,14 +97,14 @@ describe("cursor usage source", () => { ) { return { listUsage: async () => { - const report = await fetchUsage({}, (url, init) => fetchApi(url, init)); + const report = await fetchFirst((url, init) => fetchApi(url, init)); return { providers: [ { providerId: "cursor", ...report, - error: report.error ?? null, - planLabel: report.planLabel ?? null, + error: report.status === "error" ? report.error : null, + planLabel: report.status === "available" ? (report.planLabel ?? null) : null, }, ], }; @@ -250,26 +250,68 @@ describe("cursor usage source", () => { expect(cursor.status).toBe("available"); }); - it("stays unavailable when state.vscdb is unreadable", async () => { - const path = join(homeDir, ".config", "Cursor", "User", "globalStorage"); - mkdirSync(path, { recursive: true }); - writeFileSync(join(path, "state.vscdb"), "not a database"); - const cursor = findProvider(await service().listUsage(), "cursor"); - expect(cursor.status).toBe("unavailable"); + it("omits unreadable state.vscdb", async () => { + const directory = join(homeDir, ".config", "Cursor", "User", "globalStorage"); + mkdirSync(directory, { recursive: true }); + writeFileSync(join(directory, "state.vscdb"), "invalid database"); + expect(await discover()).toEqual([]); }); }); -it("identify returns a key when fetch finds cursor credentials", async () => { +it("discovery returns a locator when fetch finds cursor credentials", async () => { const previous = process.env["CURSOR_ACCESS_TOKEN"]; try { process.env["CURSOR_ACCESS_TOKEN"] = "fixture-token"; let requested = false; - await fetchUsage({}, async () => { + await fetchFirst(async () => { requested = true; return new Response(null, { status: 401 }); }); expect(requested).toBe(true); - expect(await identify()).toEqual({ key: "default" }); + expect(await discover()).toEqual([ + { key: "default", input: { store: "env", locator: "CURSOR_ACCESS_TOKEN" } }, + ]); + } finally { + if (previous === undefined) delete process.env["CURSOR_ACCESS_TOKEN"]; + else process.env["CURSOR_ACCESS_TOKEN"] = previous; + } +}); + +describe("account discovery", () => { + it.each(["empty home", "unrelated files"])("returns no accounts for %s", async (scenario) => { + const { mkdtemp, writeFile, rm } = await import("node:fs/promises"); + const directory = await mkdtemp(join(tmpdir(), "usage-empty-")); + const original = { ...process.env }; + try { + for (const key of Object.keys(process.env)) delete process.env[key]; + process.env.HOME = directory; + process.env.USERPROFILE = directory; + if (scenario === "unrelated files") await writeFile(join(directory, "unrelated.json"), "{}"); + expect(await discover()).toEqual([]); + } finally { + for (const key of Object.keys(process.env)) delete process.env[key]; + Object.assign(process.env, original); + await rm(directory, { recursive: true, force: true }); + } + }); +}); + +async function fetchFirst(fetchApi: typeof fetch) { + const accounts = await discover(); + const account = accounts[0]; + if (!account) throw new Error("No configured account"); + return fetchUsage(account.input as Parameters[0], fetchApi); +} + +it.each([401, 403])("reports an existing login rejected with HTTP %i", async (status) => { + const previous = process.env["CURSOR_ACCESS_TOKEN"]; + try { + process.env["CURSOR_ACCESS_TOKEN"] = "fixture-rejected-login"; + const report = await fetchUsage( + { store: "env", locator: "CURSOR_ACCESS_TOKEN" }, + async () => new Response(null, { status }), + ); + expect(report).toEqual({ status: "unavailable", problem: { kind: "rejected", status } }); } finally { if (previous === undefined) delete process.env["CURSOR_ACCESS_TOKEN"]; else process.env["CURSOR_ACCESS_TOKEN"] = previous; diff --git a/plugins/cursor-usage-source/server/usage.ts b/plugins/cursor-usage-source/server/usage.ts index 15a9f6e7b3..d313b1d739 100644 --- a/plugins/cursor-usage-source/server/usage.ts +++ b/plugins/cursor-usage-source/server/usage.ts @@ -7,7 +7,8 @@ import { z } from "zod"; import { toneFromUsedPct, usedPctOf, - unavailableUsage, + unavailable, + type UsageAccount, type UsageReport, type UsageBalance, } from "@getpaseo/plugin/server/usage"; @@ -114,24 +115,7 @@ function cursorTokenFromDb(db: CursorStateDatabase): string | null { return null; } -async function readCursorTokenFromSqlite(homeDir: string): Promise { - const dbPaths: string[] = []; - if (process.env["APPDATA"]) { - dbPaths.push(join(process.env["APPDATA"], "Cursor", "User", "globalStorage", "state.vscdb")); - } - dbPaths.push( - join( - homeDir, - "Library", - "Application Support", - "Cursor", - "User", - "globalStorage", - "state.vscdb", - ), - ); - dbPaths.push(join(homeDir, ".config", "Cursor", "User", "globalStorage", "state.vscdb")); - +async function readCursorTokenFromSqlite(path: string): Promise { // Held in a variable so TypeScript skips module resolution: @types/node@20 has no // node:sqlite typings yet, while the runtime (Node 22+ / Electron) provides it. const sqliteSpecifier: string = "node:sqlite"; @@ -142,24 +126,19 @@ async function readCursorTokenFromSqlite(homeDir: string): Promise { - const path = join(homeDir, ".config", "cursor", "auth.json"); +async function readCursorTokenFromAuthJson(path: string): Promise { if (!existsSync(path)) return null; try { const parsed = CursorAuthStatusSchema.parse(JSON.parse(await readFile(path, "utf8"))); @@ -173,16 +152,8 @@ export async function fetchUsage( input: UsageInput, fetchApi: typeof fetch = fetch, ): Promise { - void input; - const homeDir = homedir(); - - const token = - process.env["CURSOR_ACCESS_TOKEN"] || - process.env["CURSOR_TOKEN"] || - (await readCursorTokenFromSqlite(homeDir)) || - (await readCursorTokenFromAuthJson(homeDir)); - - if (!token) return unavailableUsage(); + const token = await readToken(input); + if (!token) throw new Error("Cursor login store no longer exists"); const res = await fetchApi( "https://api2.cursor.sh/aiserver.v1.DashboardService/GetCurrentPeriodUsage", @@ -198,9 +169,9 @@ export async function fetchUsage( }, ); - if (!res.ok) { - return unavailableUsage(); - } + if (res.status === 401 || res.status === 403) + return unavailable({ kind: "rejected", status: res.status }); + if (!res.ok) throw new Error(`Cursor usage API returned ${res.status}`); const resp = CursorUsageResponseSchema.parse(await res.json()); const billingCycleEnd = parseCursorBillingCycleTimestamp(resp.billingCycleEnd); @@ -230,12 +201,40 @@ export async function fetchUsage( }; } -export async function identify() { - const home = homedir(); +async function readToken(input: UsageInput): Promise { + if (input.store === "env") return process.env[input.locator]; const token = - process.env["CURSOR_ACCESS_TOKEN"] || - process.env["CURSOR_TOKEN"] || - (await readCursorTokenFromSqlite(home)) || - (await readCursorTokenFromAuthJson(home)); - return token ? { key: "default" } : null; + input.store === "sqlite" + ? await readCursorTokenFromSqlite(input.locator) + : await readCursorTokenFromAuthJson(input.locator); + return token ?? undefined; +} +export async function discover(): Promise { + const home = homedir(); + const candidates: UsageInput[] = ["CURSOR_ACCESS_TOKEN", "CURSOR_TOKEN"].map((locator) => ({ + store: "env", + locator, + })); + if (process.env.APPDATA) + candidates.push({ + store: "sqlite", + locator: join(process.env.APPDATA, "Cursor", "User", "globalStorage", "state.vscdb"), + }); + candidates.push( + ...[ + join( + home, + "Library", + "Application Support", + "Cursor", + "User", + "globalStorage", + "state.vscdb", + ), + join(home, ".config", "Cursor", "User", "globalStorage", "state.vscdb"), + ].map((locator) => ({ store: "sqlite" as const, locator })), + { store: "file", locator: join(home, ".config", "cursor", "auth.json") }, + ); + for (const input of candidates) if (await readToken(input)) return [{ key: "default", input }]; + return []; } diff --git a/plugins/cursor-usage-source/shared/input.ts b/plugins/cursor-usage-source/shared/input.ts index 9ee4593341..016179eba1 100644 --- a/plugins/cursor-usage-source/shared/input.ts +++ b/plugins/cursor-usage-source/shared/input.ts @@ -1,4 +1,8 @@ import { z } from "zod"; -export const inputSchema = z.object({}).strict(); - +export const inputSchema = z + .object({ + store: z.enum(["env", "file", "sqlite"]), + locator: z.string().min(1), + }) + .strict(); export type UsageInput = z.infer; diff --git a/plugins/grok-usage-source/index.server.ts b/plugins/grok-usage-source/index.server.ts index 3c87562300..15a6f4efd9 100644 --- a/plugins/grok-usage-source/index.server.ts +++ b/plugins/grok-usage-source/index.server.ts @@ -1,6 +1,6 @@ import type { PluginServerContext } from "@getpaseo/plugin/server"; import { inputSchema } from "./shared/input.js"; -import { fetchUsage, identify } from "./server/usage.js"; +import { fetchUsage, discover } from "./server/usage.js"; export default function contribute(server: PluginServerContext) { server.registerUsageSource({ @@ -8,8 +8,7 @@ export default function contribute(server: PluginServerContext) { label: "Grok", icon: "icon.svg", input: inputSchema, - discover: async () => [{}], - identify, + discover: () => discover(), fetch: fetchUsage, }); return () => {}; diff --git a/plugins/grok-usage-source/server/usage.test.ts b/plugins/grok-usage-source/server/usage.test.ts index 977c9a3fe9..0d95b0b229 100644 --- a/plugins/grok-usage-source/server/usage.test.ts +++ b/plugins/grok-usage-source/server/usage.test.ts @@ -2,7 +2,7 @@ import { mkdirSync, writeFileSync, mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { identify, fetchUsage } from "./usage.js"; +import { discover, fetchUsage } from "./usage.js"; import type { UsageReport } from "@getpaseo/plugin/server/usage"; function writeGrokAuth(home: string, auth: Record): void { @@ -70,14 +70,14 @@ describe("grok usage source", () => { ) { return { listUsage: async () => { - const report = await fetchUsage({}, (url, init) => fetchApi(url, init)); + const report = await fetchFirst((url, init) => fetchApi(url, init)); return { providers: [ { providerId: "grok", ...report, - error: report.error ?? null, - planLabel: report.planLabel ?? null, + error: report.status === "error" ? report.error : null, + planLabel: report.status === "available" ? (report.planLabel ?? null) : null, }, ], }; @@ -267,17 +267,60 @@ describe("grok usage source", () => { }); }); -it("identify returns a key when fetch finds grok credentials", async () => { +it("discovery returns a locator when fetch finds grok credentials", async () => { const previous = process.env["GROK_TOKEN"]; try { process.env["GROK_TOKEN"] = "fixture-token"; let requested = false; - await fetchUsage({}, async () => { + await fetchFirst(async () => { requested = true; return new Response(null, { status: 401 }); }); expect(requested).toBe(true); - expect(await identify()).toEqual({ key: "default" }); + expect(await discover()).toEqual([ + { key: "default", input: { store: "env", locator: "GROK_TOKEN" } }, + ]); + } finally { + if (previous === undefined) delete process.env["GROK_TOKEN"]; + else process.env["GROK_TOKEN"] = previous; + } +}); + +describe("account discovery", () => { + it.each(["empty home", "unrelated files"])("returns no accounts for %s", async (scenario) => { + const { mkdtemp, writeFile, rm } = await import("node:fs/promises"); + const directory = await mkdtemp(join(tmpdir(), "usage-empty-")); + const original = { ...process.env }; + try { + for (const key of Object.keys(process.env)) delete process.env[key]; + process.env.HOME = directory; + process.env.USERPROFILE = directory; + if (scenario === "unrelated files") await writeFile(join(directory, "unrelated.json"), "{}"); + expect(await discover()).toEqual([]); + } finally { + for (const key of Object.keys(process.env)) delete process.env[key]; + Object.assign(process.env, original); + await rm(directory, { recursive: true, force: true }); + } + }); +}); + +async function fetchFirst(fetchApi: typeof fetch) { + const accounts = await discover(); + const account = accounts[0]; + if (!account) throw new Error("No configured account"); + return fetchUsage(account.input as Parameters[0], fetchApi); +} + +it.each([401, 403])("reports an existing login rejected with HTTP %i", async (status) => { + const previous = process.env["GROK_TOKEN"]; + try { + process.env["GROK_TOKEN"] = "fixture-rejected-login"; + const report = await fetchUsage( + { store: "env", locator: "GROK_TOKEN" }, + async () => new Response(null, { status }), + ); + expect(report).toEqual({ status: "unavailable", problem: { kind: "rejected", status } }); } finally { if (previous === undefined) delete process.env["GROK_TOKEN"]; else process.env["GROK_TOKEN"] = previous; diff --git a/plugins/grok-usage-source/server/usage.ts b/plugins/grok-usage-source/server/usage.ts index 65367070b4..5f73aac6b9 100644 --- a/plugins/grok-usage-source/server/usage.ts +++ b/plugins/grok-usage-source/server/usage.ts @@ -1,12 +1,13 @@ import type { UsageInput } from "../shared/input.js"; -import { existsSync, promises as fs } from "node:fs"; +import { promises as fs } from "node:fs"; import { homedir } from "node:os"; import { join } from "node:path"; import { z } from "zod"; import { toneFromUsedPct, usedPctOf, - unavailableUsage, + unavailable, + type UsageAccount, windowFromUsedPct, type UsageReport, type UsageWindow, @@ -109,23 +110,8 @@ export async function fetchUsage( input: UsageInput, fetchApi: typeof fetch = fetch, ): Promise { - void input; - const homeDir = homedir(); - - async function readGrokToken(): Promise { - // homeDir override is for tests: Windows os.homedir() ignores $HOME (uses USERPROFILE). - const path = join(homeDir ?? homedir(), ".grok", "auth.json"); - if (!existsSync(path)) return null; - try { - return extractGrokTokenFromAuth(JSON.parse(await fs.readFile(path, "utf8"))); - } catch { - return null; - } - } - - const token = process.env["GROK_API_KEY"] || process.env["GROK_TOKEN"] || (await readGrokToken()); - - if (!token) return unavailableUsage(); + const token = await readToken(input); + if (!token) throw new Error("Grok login store no longer exists"); // The Grok CLI's /usage uses ?format=credits; without it, unified-billing accounts // get a zeroed legacy monthly shape (monthlyLimit.val 0) instead of real usage. @@ -138,9 +124,9 @@ export async function fetchUsage( }, }); - if (!res.ok) { - return unavailableUsage(); - } + if (res.status === 401 || res.status === 403) + return unavailable({ kind: "rejected", status: res.status }); + if (!res.ok) throw new Error(`Grok usage API returned ${res.status}`); const resp = GrokUsageResponseSchema.parse(await res.json()); const balance = grokMonthlyCreditBalance(resp); @@ -155,12 +141,22 @@ export async function fetchUsage( }; } -export async function identify() { - if (process.env["GROK_API_KEY"] || process.env["GROK_TOKEN"]) return { key: "default" }; +async function readToken(input: UsageInput): Promise { + if (input.store === "env") return process.env[input.locator]; try { - const auth = JSON.parse(await fs.readFile(join(homedir(), ".grok", "auth.json"), "utf8")); - return extractGrokTokenFromAuth(auth) ? { key: "default" } : null; + return ( + extractGrokTokenFromAuth(JSON.parse(await fs.readFile(input.locator, "utf8"))) ?? undefined + ); } catch { - return null; + return undefined; } } +export async function discover(): Promise { + const candidates: UsageInput[] = ["GROK_API_KEY", "GROK_TOKEN"].map((locator) => ({ + store: "env", + locator, + })); + candidates.push({ store: "file", locator: join(homedir(), ".grok", "auth.json") }); + for (const input of candidates) if (await readToken(input)) return [{ key: "default", input }]; + return []; +} diff --git a/plugins/grok-usage-source/shared/input.ts b/plugins/grok-usage-source/shared/input.ts index 9ee4593341..4be5cce316 100644 --- a/plugins/grok-usage-source/shared/input.ts +++ b/plugins/grok-usage-source/shared/input.ts @@ -1,4 +1,8 @@ import { z } from "zod"; -export const inputSchema = z.object({}).strict(); - +export const inputSchema = z + .object({ + store: z.enum(["env", "file"]), + locator: z.string().min(1), + }) + .strict(); export type UsageInput = z.infer; diff --git a/plugins/kimi-usage-source/index.server.ts b/plugins/kimi-usage-source/index.server.ts index a1df077baa..c6a9a978ff 100644 --- a/plugins/kimi-usage-source/index.server.ts +++ b/plugins/kimi-usage-source/index.server.ts @@ -1,6 +1,6 @@ import type { PluginServerContext } from "@getpaseo/plugin/server"; import { inputSchema } from "./shared/input.js"; -import { fetchUsage, identify } from "./server/usage.js"; +import { fetchUsage, discover } from "./server/usage.js"; export default function contribute(server: PluginServerContext) { server.registerUsageSource({ @@ -8,8 +8,7 @@ export default function contribute(server: PluginServerContext) { label: "Kimi", icon: "icon.svg", input: inputSchema, - discover: async () => [{}], - identify, + discover: () => discover(), fetch: fetchUsage, }); return () => {}; diff --git a/plugins/kimi-usage-source/server/usage.test.ts b/plugins/kimi-usage-source/server/usage.test.ts index 9dc83ec587..c5ba74d004 100644 --- a/plugins/kimi-usage-source/server/usage.test.ts +++ b/plugins/kimi-usage-source/server/usage.test.ts @@ -2,7 +2,7 @@ import { mkdirSync, readFileSync, writeFileSync, mkdtempSync, rmSync } from "nod import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { identify, fetchUsage } from "./usage.js"; +import { discover, fetchUsage } from "./usage.js"; import type { UsageReport } from "@getpaseo/plugin/server/usage"; function kimiCredentialPath(dir: string): string { @@ -85,14 +85,14 @@ describe("kimi usage source", () => { ) { return { listUsage: async () => { - const report = await fetchUsage({}, (url, init) => fetchApi(url, init)); + const report = await fetchFirst((url, init) => fetchApi(url, init)); return { providers: [ { providerId: "kimi", ...report, - error: report.error ?? null, - planLabel: report.planLabel ?? null, + error: report.status === "error" ? report.error : null, + planLabel: report.status === "available" ? (report.planLabel ?? null) : null, }, ], }; @@ -272,7 +272,7 @@ describe("Kimi usage source usage windows", () => { ], }), ); - const usage = await fetchUsage({}, fetchApi); + const usage = await fetchFirst(fetchApi); expect(usage).toMatchObject({ status: "available", @@ -315,7 +315,7 @@ describe("Kimi usage source usage windows", () => { ], }), ); - const usage = await fetchUsage({}, fetchApi); + const usage = await fetchFirst(fetchApi); expect(usage.windows).toHaveLength(2); expect(usage.windows[1]).toMatchObject({ @@ -340,7 +340,7 @@ describe("Kimi usage source usage windows", () => { ], }), ); - const usage = await fetchUsage({}, fetchApi); + const usage = await fetchFirst(fetchApi); expect(usage.windows).toEqual([ expect.objectContaining({ @@ -360,7 +360,7 @@ describe("Kimi usage source usage windows", () => { detail: { limit: "100", used: "10" }, }; const fetchApi = vi.fn(async () => jsonResponse({ limits: [duplicate, duplicate] })); - const usage = await fetchUsage({}, fetchApi); + const usage = await fetchFirst(fetchApi); expect(usage.windows.map((window) => window.id)).toEqual([ "coding_limit_300_time_unit_minute", @@ -369,17 +369,60 @@ describe("Kimi usage source usage windows", () => { }); }); -it("identify returns a key when fetch finds kimi credentials", async () => { +it("discovery returns a locator when fetch finds kimi credentials", async () => { const previous = process.env["KIMI_TOKEN"]; try { process.env["KIMI_TOKEN"] = "fixture-token"; let requested = false; - await fetchUsage({}, async () => { + await fetchFirst(async () => { requested = true; return new Response(null, { status: 401 }); }); expect(requested).toBe(true); - expect(await identify()).toEqual({ key: "default" }); + expect(await discover()).toEqual([ + { key: "default", input: { store: "env", locator: "KIMI_TOKEN" } }, + ]); + } finally { + if (previous === undefined) delete process.env["KIMI_TOKEN"]; + else process.env["KIMI_TOKEN"] = previous; + } +}); + +describe("account discovery", () => { + it.each(["empty home", "unrelated files"])("returns no accounts for %s", async (scenario) => { + const { mkdtemp, writeFile, rm } = await import("node:fs/promises"); + const directory = await mkdtemp(join(tmpdir(), "usage-empty-")); + const original = { ...process.env }; + try { + for (const key of Object.keys(process.env)) delete process.env[key]; + process.env.HOME = directory; + process.env.USERPROFILE = directory; + if (scenario === "unrelated files") await writeFile(join(directory, "unrelated.json"), "{}"); + expect(await discover()).toEqual([]); + } finally { + for (const key of Object.keys(process.env)) delete process.env[key]; + Object.assign(process.env, original); + await rm(directory, { recursive: true, force: true }); + } + }); +}); + +async function fetchFirst(fetchApi: typeof fetch) { + const accounts = await discover(); + const account = accounts[0]; + if (!account) throw new Error("No configured account"); + return fetchUsage(account.input as Parameters[0], fetchApi); +} + +it.each([401, 403])("reports an existing login rejected with HTTP %i", async (status) => { + const previous = process.env["KIMI_TOKEN"]; + try { + process.env["KIMI_TOKEN"] = "fixture-rejected-login"; + const report = await fetchUsage( + { store: "env", locator: "KIMI_TOKEN" }, + async () => new Response(null, { status }), + ); + expect(report).toEqual({ status: "unavailable", problem: { kind: "rejected", status } }); } finally { if (previous === undefined) delete process.env["KIMI_TOKEN"]; else process.env["KIMI_TOKEN"] = previous; diff --git a/plugins/kimi-usage-source/server/usage.ts b/plugins/kimi-usage-source/server/usage.ts index c590b1c984..095b187226 100644 --- a/plugins/kimi-usage-source/server/usage.ts +++ b/plugins/kimi-usage-source/server/usage.ts @@ -1,11 +1,12 @@ import type { UsageInput } from "../shared/input.js"; -import { existsSync, promises as fs } from "node:fs"; +import { promises as fs } from "node:fs"; import { homedir } from "node:os"; import { join } from "node:path"; import { z } from "zod"; import { toneFromUsedPct, - unavailableUsage, + unavailable, + type UsageAccount, windowFromUsedPct, type UsageReport, type UsageWindow, @@ -251,9 +252,6 @@ export async function fetchUsage( input: UsageInput, fetchApi: typeof fetch = fetch, ): Promise { - void input; - const homeDir = homedir(); - async function callUsageApi(token: string): Promise { return fetchApi(KIMI_USAGE_URL, { signal: AbortSignal.timeout(15_000), @@ -264,52 +262,19 @@ export async function fetchUsage( }); } - async function readCredentials(): Promise { - const environmentToken = process.env["KIMI_TOKEN"] || process.env["KIMI_API_KEY"]; - if (environmentToken) { - return { access_token: environmentToken }; - } - - for (const path of credentialPaths()) { - const credentials = await readCredentialFile(path); - if (credentials?.access_token) { - return { ...credentials, access_token: credentials.access_token }; - } - } - return null; - } - - function credentialPaths(): string[] { - const home = homeDir; - return [ - join( - process.env["KIMI_CODE_HOME"] || join(home, ".kimi-code"), - "credentials", - "kimi-code.json", - ), - join(home, ".kimi", "credentials", "kimi-code.json"), - ]; - } - - async function readCredentialFile(path: string): Promise { - if (!existsSync(path)) return null; - try { - return KimiAuthSchema.parse(JSON.parse(await fs.readFile(path, "utf8"))); - } catch { - return null; - } - } - - const credentials = await readCredentials(); - if (!credentials) return unavailableUsage(); + const credentials = await readCredentials(input); + if (!credentials) throw new Error("Kimi login store no longer exists"); + if (credentials.expires_at != null && credentials.expires_at * 1000 <= Date.now()) + return unavailable({ + kind: "expired", + expiresAt: new Date(credentials.expires_at * 1000).toISOString(), + }); const res = await callUsageApi(credentials.access_token); - if (!res.ok) { - // Read-only on credentials; the Kimi CLI owns refresh. See docs/providers.md. - - return unavailableUsage(); - } + if (res.status === 401 || res.status === 403) + return unavailable({ kind: "rejected", status: res.status }); + if (!res.ok) throw new Error(`Kimi usage API returned ${res.status}`); const windows = kimiUsageWindowsFromPayload(await res.json()); @@ -322,24 +287,36 @@ export async function fetchUsage( }; } -export async function identify() { - if (process.env["KIMI_TOKEN"] || process.env["KIMI_API_KEY"]) return { key: "default" }; - const home = homedir(); - const paths = [ - join( - process.env["KIMI_CODE_HOME"] || join(home, ".kimi-code"), - "credentials", - "kimi-code.json", - ), - join(home, ".kimi", "credentials", "kimi-code.json"), - ]; - for (const path of paths) { - try { - const auth = KimiAuthSchema.parse(JSON.parse(await fs.readFile(path, "utf8"))); - if (auth.access_token) return { key: "default" }; - } catch { - continue; - } +async function readCredentials(input: UsageInput): Promise { + if (input.store === "env") { + const token = process.env[input.locator]; + return token ? { access_token: token } : undefined; + } + try { + const credentials = KimiAuthSchema.parse(JSON.parse(await fs.readFile(input.locator, "utf8"))); + return credentials.access_token + ? { ...credentials, access_token: credentials.access_token } + : undefined; + } catch { + return undefined; } - return null; +} +export async function discover(): Promise { + const candidates: UsageInput[] = ["KIMI_TOKEN", "KIMI_API_KEY"].map((locator) => ({ + store: "env", + locator, + })); + candidates.push( + ...[ + join( + process.env.KIMI_CODE_HOME || join(homedir(), ".kimi-code"), + "credentials", + "kimi-code.json", + ), + join(homedir(), ".kimi", "credentials", "kimi-code.json"), + ].map((locator) => ({ store: "file" as const, locator })), + ); + for (const input of candidates) + if (await readCredentials(input)) return [{ key: "default", input }]; + return []; } diff --git a/plugins/kimi-usage-source/shared/input.ts b/plugins/kimi-usage-source/shared/input.ts index 9ee4593341..4be5cce316 100644 --- a/plugins/kimi-usage-source/shared/input.ts +++ b/plugins/kimi-usage-source/shared/input.ts @@ -1,4 +1,8 @@ import { z } from "zod"; -export const inputSchema = z.object({}).strict(); - +export const inputSchema = z + .object({ + store: z.enum(["env", "file"]), + locator: z.string().min(1), + }) + .strict(); export type UsageInput = z.infer; diff --git a/plugins/minimax-usage-source/index.server.ts b/plugins/minimax-usage-source/index.server.ts index 95d470867b..c98af742dd 100644 --- a/plugins/minimax-usage-source/index.server.ts +++ b/plugins/minimax-usage-source/index.server.ts @@ -1,6 +1,6 @@ import type { PluginServerContext } from "@getpaseo/plugin/server"; import { inputSchema } from "./shared/input.js"; -import { fetchUsage, identify } from "./server/usage.js"; +import { fetchUsage, discover } from "./server/usage.js"; export default function contribute(server: PluginServerContext) { server.registerUsageSource({ @@ -8,8 +8,7 @@ export default function contribute(server: PluginServerContext) { label: "MiniMax", icon: "icon.svg", input: inputSchema, - discover: async () => [{}], - identify, + discover: () => discover(), fetch: fetchUsage, }); return () => {}; diff --git a/plugins/minimax-usage-source/server/usage.test.ts b/plugins/minimax-usage-source/server/usage.test.ts index 8926b23980..57b58418ac 100644 --- a/plugins/minimax-usage-source/server/usage.test.ts +++ b/plugins/minimax-usage-source/server/usage.test.ts @@ -2,7 +2,7 @@ import { mkdirSync, writeFileSync, mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { identify, fetchUsage } from "./usage.js"; +import { discover, fetchUsage } from "./usage.js"; import type { UsageReport } from "@getpaseo/plugin/server/usage"; function writeMiniMaxConfig(dir: string, payload: Record): void { @@ -83,14 +83,14 @@ describe("minimax usage source", () => { ) { return { listUsage: async () => { - const report = await fetchUsage({}, (url, init) => fetchApi(url, init)); + const report = await fetchFirst((url, init) => fetchApi(url, init)); return { providers: [ { providerId: "minimax", ...report, - error: report.error ?? null, - planLabel: report.planLabel ?? null, + error: report.status === "error" ? report.error : null, + planLabel: report.status === "available" ? (report.planLabel ?? null) : null, }, ], }; @@ -154,13 +154,8 @@ describe("minimax usage source", () => { }); }); - it("returns unavailable MiniMax usage when no credentials are configured", async () => { - fetchApi = vi.fn() as never; - - const miniMax = findProvider(await service().listUsage(), "minimax"); - - expect(miniMax.status).toBe("unavailable"); - expect(fetchApi).not.toHaveBeenCalled(); + it("omits MiniMax when no credentials are configured", async () => { + expect(await discover()).toEqual([]); }); it("reads MiniMax OAuth credentials from the CLI credentials file", async () => { @@ -215,7 +210,11 @@ describe("minimax usage source", () => { const miniMax = findProvider(await service().listUsage(), "minimax"); - expect(miniMax).toMatchObject({ status: "unavailable", windows: [], error: null }); + expect(miniMax).toMatchObject({ + status: "unavailable", + problem: { kind: "no_quota" }, + error: null, + }); }); it("reports a null MiniMax token plan as unavailable even without base_resp", async () => { @@ -231,7 +230,11 @@ describe("minimax usage source", () => { const miniMax = findProvider(await service().listUsage(), "minimax"); - expect(miniMax).toMatchObject({ status: "unavailable", windows: [], error: null }); + expect(miniMax).toMatchObject({ + status: "unavailable", + problem: { kind: "no_quota" }, + error: null, + }); }); it("still reads MiniMax windows when base_resp reports success", async () => { @@ -314,17 +317,60 @@ describe("minimax usage source", () => { }); }); -it("identify returns a key when fetch finds minimax credentials", async () => { +it("discovery returns a locator when fetch finds minimax credentials", async () => { const previous = process.env["MINIMAX_API_KEY"]; try { process.env["MINIMAX_API_KEY"] = "fixture-token"; let requested = false; - await fetchUsage({}, async () => { + await fetchFirst(async () => { requested = true; return new Response(null, { status: 401 }); }); expect(requested).toBe(true); - expect(await identify()).toEqual({ key: "default" }); + expect(await discover()).toEqual([ + { key: "default", input: { store: "env", locator: "MINIMAX_API_KEY" } }, + ]); + } finally { + if (previous === undefined) delete process.env["MINIMAX_API_KEY"]; + else process.env["MINIMAX_API_KEY"] = previous; + } +}); + +describe("account discovery", () => { + it.each(["empty home", "unrelated files"])("returns no accounts for %s", async (scenario) => { + const { mkdtemp, writeFile, rm } = await import("node:fs/promises"); + const directory = await mkdtemp(join(tmpdir(), "usage-empty-")); + const original = { ...process.env }; + try { + for (const key of Object.keys(process.env)) delete process.env[key]; + process.env.HOME = directory; + process.env.USERPROFILE = directory; + if (scenario === "unrelated files") await writeFile(join(directory, "unrelated.json"), "{}"); + expect(await discover()).toEqual([]); + } finally { + for (const key of Object.keys(process.env)) delete process.env[key]; + Object.assign(process.env, original); + await rm(directory, { recursive: true, force: true }); + } + }); +}); + +async function fetchFirst(fetchApi: typeof fetch) { + const accounts = await discover(); + const account = accounts[0]; + if (!account) throw new Error("No configured account"); + return fetchUsage(account.input as Parameters[0], fetchApi); +} + +it.each([401, 403])("reports an existing login rejected with HTTP %i", async (status) => { + const previous = process.env["MINIMAX_API_KEY"]; + try { + process.env["MINIMAX_API_KEY"] = "fixture-rejected-login"; + const report = await fetchUsage( + { store: "env", locator: "MINIMAX_API_KEY" }, + async () => new Response(null, { status }), + ); + expect(report).toEqual({ status: "unavailable", problem: { kind: "rejected", status } }); } finally { if (previous === undefined) delete process.env["MINIMAX_API_KEY"]; else process.env["MINIMAX_API_KEY"] = previous; diff --git a/plugins/minimax-usage-source/server/usage.ts b/plugins/minimax-usage-source/server/usage.ts index 4ae802a285..7bf602cff1 100644 --- a/plugins/minimax-usage-source/server/usage.ts +++ b/plugins/minimax-usage-source/server/usage.ts @@ -1,10 +1,11 @@ import type { UsageInput } from "../shared/input.js"; -import { existsSync, promises as fs } from "node:fs"; +import { promises as fs } from "node:fs"; import { homedir } from "node:os"; import { join } from "node:path"; import { z } from "zod"; import { - unavailableUsage, + unavailable, + type UsageAccount, windowFromUsedPct, type UsageReport, type UsageWindow, @@ -148,83 +149,10 @@ export async function fetchUsage( input: UsageInput, fetchApi: typeof fetch = fetch, ): Promise { - void input; - const configPath = join(homedir(), ".mmx", "config.json"); - const credentialsPath = join(homedir(), ".mmx", "credentials.json"); - const env = process.env; - const now = Date.now; - - async function resolveAuth(): Promise { - const envToken = env["MINIMAX_API_KEY"]; - if (envToken) { - const envBase = env["MINIMAX_BASE_URL"]; - return { - token: envToken, - baseUrl: resolveBaseUrl({ baseUrl: envBase }), - }; - } - - const credentials = await readCredentials(); - if (credentials?.access_token && !isExpired(credentials.expires_at)) { - return { - token: credentials.access_token, - baseUrl: resolveBaseUrl({ baseUrl: credentials.resource_url }), - }; - } - - const config = await readConfig(); - if (config?.api_key) { - return { - token: config.api_key, - baseUrl: resolveBaseUrl({ - baseUrl: config.base_url, - region: config.region, - }), - }; - } - - if (config?.oauth?.access_token && !isExpired(config.oauth.expires_at)) { - return { - token: config.oauth.access_token, - baseUrl: resolveBaseUrl({ - baseUrl: config.oauth.resource_url ?? config.base_url, - region: config.region, - }), - }; - } - - return null; - } - - function isExpired(expiresAt: string | null | undefined): boolean { - if (!expiresAt) return false; - const parsed = Date.parse(expiresAt); - if (!Number.isFinite(parsed)) return false; - return parsed <= now(); - } - - async function readCredentials(): Promise | null> { - if (!existsSync(credentialsPath)) return null; - try { - const raw = JSON.parse(await fs.readFile(credentialsPath, "utf8")); - return MiniMaxCredentialsSchema.parse(raw); - } catch { - return null; - } - } - - async function readConfig(): Promise | null> { - if (!existsSync(configPath)) return null; - try { - const raw = JSON.parse(await fs.readFile(configPath, "utf8")); - return MiniMaxConfigSchema.parse(raw); - } catch { - return null; - } - } - - const auth = await resolveAuth(); - if (!auth) return unavailableUsage(); + const auth = await readAuth(input); + if (!auth) throw new Error("MiniMax login store no longer exists"); + if (auth.expiresAt && Date.parse(auth.expiresAt) <= Date.now()) + return unavailable({ kind: "expired", expiresAt: new Date(auth.expiresAt).toISOString() }); const res = await fetchApi(`${auth.baseUrl}/v1/token_plan/remains`, { signal: AbortSignal.timeout(15_000), @@ -234,15 +162,18 @@ export async function fetchUsage( }, }); - if (!res.ok) { - return unavailableUsage(); - } + if (res.status === 401 || res.status === 403) + return unavailable({ kind: "rejected", status: res.status }); + if (!res.ok) throw new Error(`MiniMax usage API returned ${res.status}`); const resp = MiniMaxQuotaResponseSchema.parse(await res.json()); const statusCode = resp.base_resp?.status_code; if (typeof statusCode === "number" && statusCode !== 0) { - return unavailableUsage(); + return unavailable({ + kind: "no_quota", + detail: resp.base_resp?.status_msg ?? "No active coding plan", + }); } const models = resp.model_remains ?? []; @@ -256,8 +187,10 @@ export async function fetchUsage( if (weeklyWindow) windows.push(weeklyWindow); } + if (windows.length === 0) + return unavailable({ kind: "no_quota", detail: "No active coding plan" }); return { - status: windows.length > 0 ? "available" : "unavailable", + status: "available", planLabel: undefined, windows, balances: [], @@ -265,24 +198,55 @@ export async function fetchUsage( }; } -export async function identify() { - if (process.env["MINIMAX_API_KEY"]) return { key: "default" }; - const home = join(homedir(), ".mmx"); - try { - const credentials = MiniMaxCredentialsSchema.parse( - JSON.parse(await fs.readFile(join(home, "credentials.json"), "utf8")), - ); - if (credentials.access_token) return { key: "default" }; - } catch { - /* Check config next. */ +async function readAuth( + input: UsageInput, +): Promise<(MiniMaxResolvedAuth & { expiresAt?: string }) | undefined> { + if (input.store === "env") { + const token = process.env[input.locator]; + return token + ? { token, baseUrl: resolveBaseUrl({ baseUrl: process.env.MINIMAX_BASE_URL }) } + : undefined; } try { - const config = MiniMaxConfigSchema.parse( - JSON.parse(await fs.readFile(join(home, "config.json"), "utf8")), - ); - if (config.api_key || config.oauth?.access_token) return { key: "default" }; + const raw: unknown = JSON.parse(await fs.readFile(input.locator, "utf8")); + if (input.store === "credentials") { + const credentials = MiniMaxCredentialsSchema.parse(raw); + return credentials.access_token + ? { + token: credentials.access_token, + baseUrl: resolveBaseUrl({ baseUrl: credentials.resource_url }), + expiresAt: credentials.expires_at ?? undefined, + } + : undefined; + } + const config = MiniMaxConfigSchema.parse(raw); + if (config.api_key) + return { + token: config.api_key, + baseUrl: resolveBaseUrl({ baseUrl: config.base_url, region: config.region }), + }; + return config.oauth?.access_token + ? { + token: config.oauth.access_token, + baseUrl: resolveBaseUrl({ + baseUrl: config.oauth.resource_url ?? config.base_url, + region: config.region, + }), + expiresAt: config.oauth.expires_at ?? undefined, + } + : undefined; } catch { - /* No configured account. */ + return undefined; } - return null; +} +export async function discover(): Promise { + const candidates: UsageInput[] = [ + { store: "env", locator: "MINIMAX_API_KEY" }, + { store: "credentials", locator: join(homedir(), ".mmx", "credentials.json") }, + { store: "config", locator: join(homedir(), ".mmx", "config.json") }, + ]; + const accounts: UsageAccount[] = []; + for (const input of candidates) + if (await readAuth(input)) accounts.push({ key: "default", input }); + return accounts; } diff --git a/plugins/minimax-usage-source/shared/input.ts b/plugins/minimax-usage-source/shared/input.ts index 9ee4593341..22e12a603f 100644 --- a/plugins/minimax-usage-source/shared/input.ts +++ b/plugins/minimax-usage-source/shared/input.ts @@ -1,4 +1,8 @@ import { z } from "zod"; -export const inputSchema = z.object({}).strict(); - +export const inputSchema = z + .object({ + store: z.enum(["env", "credentials", "config"]), + locator: z.string().min(1), + }) + .strict(); export type UsageInput = z.infer; diff --git a/plugins/muse-provider/server/usage.ts b/plugins/muse-provider/server/usage.ts index 36dae811d8..85cfb473b6 100644 --- a/plugins/muse-provider/server/usage.ts +++ b/plugins/muse-provider/server/usage.ts @@ -5,7 +5,7 @@ import type { UsageSourceRegistration } from "@getpaseo/plugin/server"; import type { ProviderLaunch } from "@getpaseo/plugin/server/provider"; import { hashAccountKey, - unavailableUsage, + unavailable, windowFromUsedPct, toneFromUsedPct, } from "@getpaseo/plugin/server/usage"; @@ -59,18 +59,19 @@ export class Usage { label: "Muse Code", icon: "icon.svg", input: inputSchema, - discover: async () => [...this.launches.keys()].map((account) => ({ account })), - identify: async (input) => { - const { account } = inputSchema.parse(input); - return this.launches.has(account) ? { key: account, label: "Muse Code" } : null; - }, + discover: async () => + [...this.launches.keys()].map((account) => ({ + key: account, + label: "Muse Code", + input: { account }, + })), fetch: async (input) => { const { account } = inputSchema.parse(input); const launch = this.launches.get(account); - if (!launch) return unavailableUsage(); + if (!launch) throw new Error("Muse login no longer exists"); try { const { usage } = await this.read(account, launch); - if (!usage) return unavailableUsage(); + if (!usage) return unavailable({ kind: "no_quota", detail: "No usage quota reported" }); return { status: "available", planLabel: usage.tier, @@ -105,7 +106,6 @@ export class Usage { } catch (error) { return { status: "error", - windows: [], error: error instanceof Error ? error.message : String(error), }; } diff --git a/plugins/muse-provider/test/provider.test.ts b/plugins/muse-provider/test/provider.test.ts index 1d346a0c29..4393cda7ee 100644 --- a/plugins/muse-provider/test/provider.test.ts +++ b/plugins/muse-provider/test/provider.test.ts @@ -1157,12 +1157,13 @@ for (const usage of [ await h.open(); const inputs = await h.usageSource.discover(); expect(inputs).toHaveLength(1); - const input = inputs[0]; - expect(await h.usageSource.identify(input)).toEqual({ - key: (input as { account: string }).account, + const account = inputs[0]!; + expect(account).toEqual({ + key: expect.stringMatching(/^[a-f0-9]{64}$/), label: "Muse Code", + input: { account: account.key }, }); - const report = await h.usageSource.fetch(input); + const report = await h.usageSource.fetch(account.input); if ("usage" in usage) expect(report).toMatchObject({ status: "available", @@ -1178,7 +1179,11 @@ for (const usage of [ { id: "weekly", usedPct: 105, remainingPct: 0, tone: "danger" }, ], }); - else expect(report).toEqual({ status: "unavailable", windows: [], balances: [], details: [] }); + else + expect(report).toEqual({ + status: "unavailable", + problem: { kind: "no_quota", detail: "No usage quota reported" }, + }); expect((await h.recorded()).filter((f) => f.method === "usage/read")).toHaveLength(1); }); } @@ -1303,10 +1308,13 @@ test("usage identity follows the resolved config directory across credential and await h.open(); const inputs = await h.usageSource.discover(); expect(inputs).toHaveLength(1); - const identity = await h.usageSource.identify(inputs[0]); - expect(identity).not.toBeNull(); - identities.push(identity!.key); + identities.push(inputs[0]!.key); } expect(identities[0]).toBe(identities[1]); expect(identities[2]).not.toBe(identities[0]); }); + +test.each(["no sessions", "unrelated environment"])("Muse discovery is empty with %s", async () => { + const { Usage } = await import("../server/usage.js"); + expect(await new Usage().registration().discover()).toEqual([]); +}); diff --git a/plugins/opencode-go-usage-source/index.server.ts b/plugins/opencode-go-usage-source/index.server.ts index cb47cc1ac6..b011eaf2f4 100644 --- a/plugins/opencode-go-usage-source/index.server.ts +++ b/plugins/opencode-go-usage-source/index.server.ts @@ -1,6 +1,6 @@ import type { PluginServerContext } from "@getpaseo/plugin/server"; import { inputSchema } from "./shared/input.js"; -import { discover, fetchUsage, identify } from "./server/usage.js"; +import { discover, fetchUsage } from "./server/usage.js"; export default function contribute(server: PluginServerContext) { server.registerUsageSource({ @@ -9,7 +9,6 @@ export default function contribute(server: PluginServerContext) { icon: "icon.svg", input: inputSchema, discover, - identify, fetch: fetchUsage, }); return () => {}; diff --git a/plugins/opencode-go-usage-source/server/usage.test.ts b/plugins/opencode-go-usage-source/server/usage.test.ts index 849fbe874f..72fbe0ee13 100644 --- a/plugins/opencode-go-usage-source/server/usage.test.ts +++ b/plugins/opencode-go-usage-source/server/usage.test.ts @@ -2,7 +2,7 @@ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, beforeEach, expect, test } from "vitest"; -import { discover, fetchUsage, identify } from "./usage.js"; +import { discover, fetchUsage } from "./usage.js"; import { inputSchema } from "../shared/input.js"; @@ -22,7 +22,7 @@ afterEach(async () => { test("discovery input rejects an explicit API key", () => { expect(inputSchema.safeParse({ apiKey: "unused" }).success).toBe(false); - expect(inputSchema.parse({})).toEqual({}); + expect(() => inputSchema.parse({})).toThrow(); }); const upstreamResponse = { @@ -39,7 +39,9 @@ test("discovers and fetches the default key from read-only auth.json", async () const content = JSON.stringify({ "opencode-go": { type: "api", key: "fixture-key" } }); try { await writeFile(path, content); - expect(await discover(path)).toEqual([{}]); + expect(await discover(path)).toEqual([ + { key: expect.stringMatching(/^[a-f0-9]{64}$/), input: { path } }, + ]); const requests: Array<{ url: string; authorization: string | null }> = []; const fetchApi = async (url: string | URL | Request, init?: RequestInit) => { requests.push({ @@ -48,8 +50,7 @@ test("discovers and fetches the default key from read-only auth.json", async () }); return Response.json(upstreamResponse); }; - const report = await fetchUsage({}, fetchApi as typeof fetch, path); - expect((await identify({}, path))?.key).toMatch(/^[a-f0-9]{64}$/); + const report = await fetchUsage({ path }, fetchApi as typeof fetch); expect(requests).toEqual([ { url: "https://opencode.ai/zen/go/v1/usage", authorization: "Bearer fixture-key" }, ]); @@ -75,38 +76,29 @@ test("omits default discovery when auth.json lacks an OpenCode Go API key", asyn try { await writeFile(path, JSON.stringify({ openai: { type: "oauth", access: "other-token" } })); expect(await discover(path)).toEqual([]); - const report = await fetchUsage( - {}, - async () => { + await expect( + fetchUsage({ path }, async () => { throw new Error("must not fetch"); - }, - path, - ); - expect(report).toEqual({ status: "unavailable", windows: [] }); + }), + ).rejects.toThrow("login store no longer exists"); } finally { await rm(directory, { recursive: true, force: true }); } }); test.each([401, 403])("maps HTTP %i to unavailable", async (status) => { - const report = await fetchUsage({}, async () => new Response(null, { status }), fixturePath); - expect(report.status).toBe("unavailable"); - expect(report.windows).toEqual([]); + const report = await fetchUsage( + { path: fixturePath }, + async () => new Response(null, { status }), + ); + expect(report).toEqual({ + status: "unavailable", + problem: { kind: "rejected", status, refreshedBy: "opencode" }, + }); }); -test("identify returns a key when fetch finds OpenCode Go credentials", async () => { - let requested = false; - await fetchUsage( - {}, - async () => { - requested = true; - return new Response(null, { status: 401 }); - }, - fixturePath, - ); - expect(requested).toBe(true); - const identity = await identify({}, fixturePath); - expect(identity?.key).toMatch(/^[a-f0-9]{64}$/); - expect(identity?.key).not.toBe("fixture-secret"); - expect(await identify({}, fixturePath)).toEqual(identity); +test.each(["missing file", "unrelated file"])("discovers no accounts for %s", async (scenario) => { + const path = join(fixtureDirectory, "missing.json"); + if (scenario === "unrelated file") await writeFile(path, "{}"); + expect(await discover(path)).toEqual([]); }); diff --git a/plugins/opencode-go-usage-source/server/usage.ts b/plugins/opencode-go-usage-source/server/usage.ts index 77e83613df..cd2905ca2e 100644 --- a/plugins/opencode-go-usage-source/server/usage.ts +++ b/plugins/opencode-go-usage-source/server/usage.ts @@ -4,6 +4,8 @@ import { join } from "node:path"; import { z } from "zod"; import { hashAccountKey, + unavailable, + type UsageAccount, toneFromUsedPct, windowFromUsedPct, type UsageReport, @@ -44,23 +46,22 @@ export async function readDefaultKey(path = authPath()): Promise } } -export async function discover(path = authPath()): Promise> { - return (await readDefaultKey(path)) ? [{}] : []; +export async function discover(path = authPath()): Promise { + return (await readDefaultKey(path)) ? [{ key: hashAccountKey(path), input: { path } }] : []; } export async function fetchUsage( - _input: Input, + input: Input, fetchApi: typeof fetch = fetch, - path = authPath(), ): Promise { - const apiKey = await readDefaultKey(path); - if (!apiKey) return { status: "unavailable", windows: [] }; + const apiKey = await readDefaultKey(input.path); + if (!apiKey) throw new Error("OpenCode Go login store no longer exists"); const response = await fetchApi("https://opencode.ai/zen/go/v1/usage", { headers: { Authorization: `Bearer ${apiKey}`, Accept: "application/json" }, signal: AbortSignal.timeout(15_000), }); if (response.status === 401 || response.status === 403) - return { status: "unavailable", windows: [] }; + return unavailable({ kind: "rejected", status: response.status, refreshedBy: "opencode" }); if (!response.ok) throw new Error(`OpenCode Go usage API returned ${response.status}`); const data = responseSchema.parse(await response.json()); const windows = ( @@ -82,8 +83,3 @@ export async function fetchUsage( ); return { status: "available", planLabel: "Go", windows }; } - -export async function identify(_input: Input, path = authPath()) { - const key = await readDefaultKey(path); - return key ? { key: hashAccountKey(key) } : null; -} diff --git a/plugins/opencode-go-usage-source/shared/input.ts b/plugins/opencode-go-usage-source/shared/input.ts index 9ba7acfb94..6e3ffd1719 100644 --- a/plugins/opencode-go-usage-source/shared/input.ts +++ b/plugins/opencode-go-usage-source/shared/input.ts @@ -1,4 +1,3 @@ import { z } from "zod"; - -export const inputSchema = z.object({}).strict(); +export const inputSchema = z.object({ path: z.string().min(1) }).strict(); export type Input = z.infer; diff --git a/plugins/zai-usage-source/index.server.ts b/plugins/zai-usage-source/index.server.ts index b4fd86943a..78dfdf7fda 100644 --- a/plugins/zai-usage-source/index.server.ts +++ b/plugins/zai-usage-source/index.server.ts @@ -1,6 +1,6 @@ import type { PluginServerContext } from "@getpaseo/plugin/server"; import { inputSchema } from "./shared/input.js"; -import { fetchUsage, identify } from "./server/usage.js"; +import { fetchUsage, discover } from "./server/usage.js"; export default function contribute(server: PluginServerContext) { server.registerUsageSource({ @@ -8,8 +8,7 @@ export default function contribute(server: PluginServerContext) { label: "Z.ai", icon: "icon.svg", input: inputSchema, - discover: async () => [{}], - identify, + discover: () => discover(), fetch: fetchUsage, }); return () => {}; diff --git a/plugins/zai-usage-source/server/usage.test.ts b/plugins/zai-usage-source/server/usage.test.ts index 164543c00d..d63602cd73 100644 --- a/plugins/zai-usage-source/server/usage.test.ts +++ b/plugins/zai-usage-source/server/usage.test.ts @@ -2,7 +2,7 @@ import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { identify, fetchUsage } from "./usage.js"; +import { discover, fetchUsage } from "./usage.js"; import type { UsageReport } from "@getpaseo/plugin/server/usage"; function mockFetch(handlers: Map Response>): typeof fetch { @@ -65,14 +65,14 @@ describe("zai usage source", () => { ) { return { listUsage: async () => { - const report = await fetchUsage({}, (url, init) => fetchApi(url, init)); + const report = await fetchFirst((url, init) => fetchApi(url, init)); return { providers: [ { providerId: "zai", ...report, - error: report.error ?? null, - planLabel: report.planLabel ?? null, + error: report.status === "error" ? report.error : null, + planLabel: report.status === "available" ? (report.planLabel ?? null) : null, }, ], }; @@ -118,17 +118,60 @@ describe("zai usage source", () => { }); }); -it("identify returns a key when fetch finds zai credentials", async () => { +it("discovery returns a locator when fetch finds zai credentials", async () => { const previous = process.env["ZAI_API_KEY"]; try { process.env["ZAI_API_KEY"] = "fixture-token"; let requested = false; - await fetchUsage({}, async () => { + await fetchFirst(async () => { requested = true; return new Response(null, { status: 401 }); }); expect(requested).toBe(true); - expect(await identify()).toEqual({ key: "default" }); + expect(await discover()).toEqual([ + { key: "default", input: { store: "env", locator: "ZAI_API_KEY" } }, + ]); + } finally { + if (previous === undefined) delete process.env["ZAI_API_KEY"]; + else process.env["ZAI_API_KEY"] = previous; + } +}); + +describe("account discovery", () => { + it.each(["empty home", "unrelated files"])("returns no accounts for %s", async (scenario) => { + const { mkdtemp, writeFile, rm } = await import("node:fs/promises"); + const directory = await mkdtemp(join(tmpdir(), "usage-empty-")); + const original = { ...process.env }; + try { + for (const key of Object.keys(process.env)) delete process.env[key]; + process.env.HOME = directory; + process.env.USERPROFILE = directory; + if (scenario === "unrelated files") await writeFile(join(directory, "unrelated.json"), "{}"); + expect(await discover()).toEqual([]); + } finally { + for (const key of Object.keys(process.env)) delete process.env[key]; + Object.assign(process.env, original); + await rm(directory, { recursive: true, force: true }); + } + }); +}); + +async function fetchFirst(fetchApi: typeof fetch) { + const accounts = await discover(); + const account = accounts[0]; + if (!account) throw new Error("No configured account"); + return fetchUsage(account.input as Parameters[0], fetchApi); +} + +it.each([401, 403])("reports an existing login rejected with HTTP %i", async (status) => { + const previous = process.env["ZAI_API_KEY"]; + try { + process.env["ZAI_API_KEY"] = "fixture-rejected-login"; + const report = await fetchUsage( + { store: "env", locator: "ZAI_API_KEY" }, + async () => new Response(null, { status }), + ); + expect(report).toEqual({ status: "unavailable", problem: { kind: "rejected", status } }); } finally { if (previous === undefined) delete process.env["ZAI_API_KEY"]; else process.env["ZAI_API_KEY"] = previous; diff --git a/plugins/zai-usage-source/server/usage.ts b/plugins/zai-usage-source/server/usage.ts index 432990f374..9c07fa6d4e 100644 --- a/plugins/zai-usage-source/server/usage.ts +++ b/plugins/zai-usage-source/server/usage.ts @@ -1,7 +1,8 @@ import type { UsageInput } from "../shared/input.js"; import { z } from "zod"; import { - unavailableUsage, + unavailable, + type UsageAccount, type UsageReport, type UsageDetail, } from "@getpaseo/plugin/server/usage"; @@ -28,10 +29,8 @@ export async function fetchUsage( input: UsageInput, fetchApi: typeof fetch = fetch, ): Promise { - void input; - - const token = process.env["ZAI_API_KEY"] || process.env["GLM_API_KEY"]; - if (!token) return unavailableUsage(); + const token = process.env[input.locator]; + if (!token) throw new Error("Z.ai login store no longer exists"); const res = await fetchApi("https://api.z.ai/api/biz/subscription/list", { signal: AbortSignal.timeout(15_000), @@ -41,13 +40,13 @@ export async function fetchUsage( }, }); - if (!res.ok) { - return unavailableUsage(); - } + if (res.status === 401 || res.status === 403) + return unavailable({ kind: "rejected", status: res.status }); + if (!res.ok) throw new Error(`Z.ai usage API returned ${res.status}`); const resp = ZaiUsageResponseSchema.parse(await res.json()); const sub = resp.data?.[0]; - if (!sub) return unavailableUsage(); + if (!sub) return unavailable({ kind: "no_quota", detail: "No active coding plan" }); const details: UsageDetail[] = []; if (sub.status) details.push({ id: "status", label: "Status", value: sub.status }); @@ -65,6 +64,9 @@ export async function fetchUsage( }; } -export async function identify() { - return process.env["ZAI_API_KEY"] || process.env["GLM_API_KEY"] ? { key: "default" } : null; +export async function discover(): Promise { + for (const locator of ["ZAI_API_KEY", "GLM_API_KEY"]) { + if (process.env[locator]) return [{ key: "default", input: { store: "env", locator } }]; + } + return []; } diff --git a/plugins/zai-usage-source/shared/input.ts b/plugins/zai-usage-source/shared/input.ts index 9ee4593341..0d34ddf4e4 100644 --- a/plugins/zai-usage-source/shared/input.ts +++ b/plugins/zai-usage-source/shared/input.ts @@ -1,4 +1,8 @@ import { z } from "zod"; -export const inputSchema = z.object({}).strict(); - +export const inputSchema = z + .object({ + store: z.literal("env"), + locator: z.string().min(1), + }) + .strict(); export type UsageInput = z.infer; diff --git a/public-docs/plugins/reference.md b/public-docs/plugins/reference.md index 80c21f4e8b..f92a6b06a4 100644 --- a/public-docs/plugins/reference.md +++ b/public-docs/plugins/reference.md @@ -309,13 +309,78 @@ SVG or URL. ### Usage sources -**Requires Paseo 0.9.3 or newer.** Server plugins register a usage source with `server.registerUsageSource()` and import types and helpers from `@getpaseo/plugin/server/usage`. +**Requires Paseo 0.11.** Register a source from your server entry with +`server.registerUsageSource()`. Import its contract and helpers from +`@getpaseo/plugin/server/usage`. + +A source implements two calls: + +```ts +interface UsageAccount { + key: string; + label?: string; + input: JsonValue; +} + +interface UsageSourceRegistration { + id: string; + label: string; + icon?: string; + input: ZodType; + discover(): Promise; + fetch(input: unknown): Promise; +} + +type UsageReport = + | { + status: "available"; + planLabel?: string; + windows: UsageWindow[]; + balances?: UsageBalance[]; + details?: UsageDetail[]; + } + | { status: "unavailable"; problem: UsageProblem } + | { status: "error"; error: string }; + +type UsageProblem = + | { kind: "expired"; expiresAt: string; refreshedBy?: string } + | { kind: "rejected"; status: number; refreshedBy?: string } + | { kind: "no_quota"; detail: string }; +``` + +Return every account whose login exists on the machine from `discover()`, including expired +logins. Return `[]` when none exists. Discovery takes no arguments and must work independently of +agent sessions and provider names. Inputs name credential stores; never put credentials in inputs +or reports. Paseo validates each input against your schema before calling `fetch()`. + +Use a stable account key: 1–128 characters from `[A-Za-z0-9._-]`. It identifies the account or +organization whose quota is metered and survives token rotation. Never use a credential or raw +email as the key. Use `hashAccountKey()` for sensitive stable identities or a store locator when +account metadata is unavailable. Labels can name accounts without becoming their identity. + +Return logins in preference order. Several entries with the same key become one card, with their +inputs tried in order until a report is `available`. An unavailable report, error report, or thrown +fetch falls through to the next input. If none succeeds, the card carries the last report. +Discovery failures are logged by the daemon and produce no card. + +Re-read the selected store in `fetch()` so the CLI's token rotations take effect. Never redeem +refresh tokens or write credential stores: refreshing elsewhere can invalidate the CLI's copy, +and rewriting parsed files can discard fields you do not model. If the store disappeared after +discovery, throw; the card shows the error until the next discovery removes it. + +Use `unavailable(problem)` to explain why an existing login cannot supply quota. It requires a +problem; there is no zero-argument form. `expiresAt` is an ISO timestamp. A rejected login carries +the upstream HTTP status. `refreshedBy` is a CLI name, such as `claude`, `codex`, `opencode`, `omp`, or +`pi`. Paseo owns the remedy sentence. Do not put instructions or user-facing sentences in that +field. For `no_quota`, `detail` is displayed verbatim. ```ts import { z } from "zod"; import type { PluginServerContext } from "@getpaseo/plugin/server"; +import { hashAccountKey, unavailable } from "@getpaseo/plugin/server/usage"; +import { findLoginStores, readLogin, readQuota } from "./server/logins"; -const input = z.object({ account: z.string() }); +const input = z.object({ path: z.string() }).strict(); export default function contribute(server: PluginServerContext) { server.registerUsageSource({ @@ -323,31 +388,42 @@ export default function contribute(server: PluginServerContext) { label: "Example", icon: "icon.svg", input, - discover: async () => [{ account: "default" }], - identify: async (value) => { - const { account } = input.parse(value); - return { key: account }; + discover: async () => + (await findLoginStores()).map((path) => ({ + key: hashAccountKey(path), + input: { path }, + })), + fetch: async (value) => { + const { path } = input.parse(value); + const login = await readLogin(path); + if (login.expiresAt <= Date.now()) + return unavailable({ + kind: "expired", + expiresAt: new Date(login.expiresAt).toISOString(), + refreshedBy: "example", + }); + return readQuota(login); }, - fetch: async () => ({ status: "available", windows: [] }), }); return () => {}; } ``` -`discover()` is required and supplies configured inputs; return `[]` when no account is configured. `identify(input)` returns a stable account key and optional display label without fetching usage, or `null` when there are no credentials. The daemon combines the source ID and key as `:`. The key must be 1–128 characters from `[A-Za-z0-9._-]`, remain stable across token rotation, and identify the account or organization whose quota is metered. Never use a credential or raw email as the key; use `hashAccountKey(value)` when the only stable identity is sensitive. +Built-in Claude discovery prefers the macOS Keychain login and uses the Claude Code credential +file only when Keychain is empty. `CLAUDE_CONFIG_DIR` selects that file's directory. Fresh tokens +use the OAuth profile's account and organization IDs; expired or rejected profiles use a locator +hash. Codex prefers Codex CLI, OpenCode, Pi, then OMP and groups by the ChatGPT account ID from stored +metadata or JWT claims. Pi and OMP logins remain discoverable when expired. OMP requires +`node:sqlite`. -Return credential-store locators in preference order from `discover()`. The daemon groups inputs with the same source and account key into one card, keeps that order, and tries each login until `fetch()` returns `available`. An unavailable or error report, thrown failure, or missing login falls through to the next input. If none succeeds, the card carries the last attempted report (or `unavailable` if no login remains). Before fetching, the daemon identifies the login again and skips it if its account changed. Re-read credentials in `fetch()` so the harness's token rotations take effect; never refresh or write its tokens. Keep raw credentials out of discovery inputs and reports. +`usage.list_reports` discovers accounts when called without IDs. With IDs, it refreshes known +accounts without rediscovering identity. If a store switches accounts, the existing card shows the +new login's quota until the next discovery. Reports are cached for five minutes; `forceRefresh` +bypasses that cache. Entries carry `id`, `account.label`, and `fetchedAt`. Clients gate the feature +on `server_info.features.usageSources`. The older `provider.usage.list` RPC maps the same reports +for 0.10 clients and renders problems into its `error` string. -Built-in subscription sources discover logins independently of agents and provider names: - -| Source | Login preference order | Account key | -| ------ | -------------------------------------------------------- | ----------------------------------------------------- | -| Codex | Codex CLI (`$CODEX_HOME`, `~/.codex`), OpenCode, Pi, OMP | ChatGPT account ID from stored metadata or JWT claims | -| Claude | Claude Code credential file, macOS keychain, Pi, OMP | OAuth account UUID and organization UUID | - -OpenCode honors `XDG_DATA_HOME`; Pi honors `PI_CODING_AGENT_DIR`. OMP honors its directory, profile, and XDG settings, reads enabled unexpired OAuth rows by credential ID, and is skipped when the host lacks `node:sqlite`. Claude Code honors `CLAUDE_CONFIG_DIR`. Each Claude login's OAuth profile supplies its identity. - -`usage.list_reports` discovers reports when called without IDs, or reads only the requested known IDs. It caches each report for five minutes and `forceRefresh` refreshes only the returned IDs. Each entry carries `id`, `account.label`, and `fetchedAt`; `fetch()` returns a `UsageReport` with `status` (`available`, `unavailable`, or `error`), optional `planLabel`, and generic `windows`, `balances`, and `details`. The icon is a path to a self-contained SVG under the plugin directory and follows the provider icon restrictions above. +The source icon follows the provider SVG restrictions above. ## Entry point and cleanup From 31cd18e74891de7c8360ead61ea312c69d97b662 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Fri, 2 Oct 2026 01:57:28 +0200 Subject: [PATCH 053/100] docs(changelog): add 0.11.0-beta.3 release notes --- CHANGELOG.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f917a4f156..0534c54505 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## 0.11.0-beta.3 - 2026-10-02 + +### Added + +- Added a pin button to Usage window rows, shown on hover and filled when the window is pinned ([#5876](https://github.com/getpaseo/paseo/pull/5876)) + +### Changed + +- Changed usage source plugins to implement `discover()` and `fetch()` instead of `identify()` ([#5876](https://github.com/getpaseo/paseo/pull/5876)) + +### Fixed + +- Fixed a Usage card disappearing when its login expired; the card now shows the expiry and the command that refreshes it ([#5876](https://github.com/getpaseo/paseo/pull/5876)) +- Fixed a Usage card disappearing when its login was rejected; the card now shows the HTTP status ([#5876](https://github.com/getpaseo/paseo/pull/5876)) +- Fixed an extra Usage card appearing when a usage source failed to discover accounts ([#5876](https://github.com/getpaseo/paseo/pull/5876)) + ## 0.11.0-beta.2 - 2026-10-01 ### Added From 6166a7aca5e3184e8ab505caad28f6a595816414 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Fri, 2 Oct 2026 02:15:43 +0200 Subject: [PATCH 054/100] chore(release): cut 0.11.0-beta.3 --- package-lock.json | 56 ++++++++++++------------ package.json | 2 +- packages/app/package.json | 2 +- packages/cli/package.json | 8 ++-- packages/client/package.json | 6 +-- packages/desktop/package.json | 2 +- packages/expo-two-way-audio/package.json | 2 +- packages/highlight/package.json | 2 +- packages/plugin/package.json | 10 ++--- packages/protocol/package.json | 2 +- packages/relay/package.json | 2 +- packages/server/package.json | 12 ++--- packages/website/package.json | 2 +- plugins/package.json | 2 +- 14 files changed, 55 insertions(+), 55 deletions(-) diff --git a/package-lock.json b/package-lock.json index e346f1a70c..0e65c23a46 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "paseo", - "version": "0.11.0-beta.2", + "version": "0.11.0-beta.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "paseo", - "version": "0.11.0-beta.2", + "version": "0.11.0-beta.3", "hasInstallScript": true, "license": "Apache-2.0", "workspaces": [ @@ -36415,7 +36415,7 @@ }, "packages/app": { "name": "@getpaseo/app", - "version": "0.11.0-beta.2", + "version": "0.11.0-beta.3", "dependencies": { "@codemirror/commands": "6.10.4", "@codemirror/language": "6.12.4", @@ -37481,12 +37481,12 @@ }, "packages/cli": { "name": "@getpaseo/cli", - "version": "0.11.0-beta.2", + "version": "0.11.0-beta.3", "dependencies": { "@clack/prompts": "^1.0.0", - "@getpaseo/client": "0.11.0-beta.2", - "@getpaseo/protocol": "0.11.0-beta.2", - "@getpaseo/server": "0.11.0-beta.2", + "@getpaseo/client": "0.11.0-beta.3", + "@getpaseo/protocol": "0.11.0-beta.3", + "@getpaseo/server": "0.11.0-beta.3", "chalk": "^5.3.0", "commander": "^12.0.0", "mime-types": "^2.1.35", @@ -37733,10 +37733,10 @@ }, "packages/client": { "name": "@getpaseo/client", - "version": "0.11.0-beta.2", + "version": "0.11.0-beta.3", "dependencies": { - "@getpaseo/protocol": "0.11.0-beta.2", - "@getpaseo/relay": "0.11.0-beta.2", + "@getpaseo/protocol": "0.11.0-beta.3", + "@getpaseo/relay": "0.11.0-beta.3", "zod": "^4.4.3" }, "devDependencies": { @@ -37747,7 +37747,7 @@ }, "packages/desktop": { "name": "@getpaseo/desktop", - "version": "0.11.0-beta.2", + "version": "0.11.0-beta.3", "license": "Apache-2.0", "dependencies": { "@getpaseo/cli": "*", @@ -37991,7 +37991,7 @@ }, "packages/expo-two-way-audio": { "name": "@getpaseo/expo-two-way-audio", - "version": "0.11.0-beta.2", + "version": "0.11.0-beta.3", "license": "MIT", "devDependencies": { "@types/jest": "^29.5.14", @@ -38887,7 +38887,7 @@ }, "packages/highlight": { "name": "@getpaseo/highlight", - "version": "0.11.0-beta.2", + "version": "0.11.0-beta.3", "dependencies": { "@codemirror/language": "6.12.4", "@codemirror/legacy-modes": "^6.5.3", @@ -38916,14 +38916,14 @@ }, "packages/plugin": { "name": "@getpaseo/plugin", - "version": "0.11.0-beta.2", + "version": "0.11.0-beta.3", "dependencies": { "@agentclientprotocol/sdk": "^1.4.0", "use-sync-external-store": "^1.6.0" }, "devDependencies": { - "@getpaseo/client": "0.11.0-beta.2", - "@getpaseo/protocol": "0.11.0-beta.2", + "@getpaseo/client": "0.11.0-beta.3", + "@getpaseo/protocol": "0.11.0-beta.3", "@types/node": "^20.9.0", "@types/react": "~19.2.0", "react": "19.1.0", @@ -38933,8 +38933,8 @@ "zod": "^4.4.3" }, "peerDependencies": { - "@getpaseo/client": "0.11.0-beta.2", - "@getpaseo/protocol": "0.11.0-beta.2", + "@getpaseo/client": "0.11.0-beta.3", + "@getpaseo/protocol": "0.11.0-beta.3", "react": "~19.1.0", "react-native": ">=0.81.5", "zod": "^4.4.3" @@ -38956,7 +38956,7 @@ }, "packages/protocol": { "name": "@getpaseo/protocol", - "version": "0.11.0-beta.2", + "version": "0.11.0-beta.3", "dependencies": { "semver": "^7.8.5", "zod": "^4.4.3" @@ -38983,7 +38983,7 @@ }, "packages/relay": { "name": "@getpaseo/relay", - "version": "0.11.0-beta.2", + "version": "0.11.0-beta.3", "dependencies": { "base64-js": "^1.5.1", "tweetnacl": "^1.0.3", @@ -39201,16 +39201,16 @@ }, "packages/server": { "name": "@getpaseo/server", - "version": "0.11.0-beta.2", + "version": "0.11.0-beta.3", "dependencies": { "@agentclientprotocol/sdk": "^0.17.1", "@anthropic-ai/claude-agent-sdk": "0.3.246", "@anthropic-ai/sdk": "^0.104.2", - "@getpaseo/client": "0.11.0-beta.2", - "@getpaseo/highlight": "0.11.0-beta.2", - "@getpaseo/plugin": "0.11.0-beta.2", - "@getpaseo/protocol": "0.11.0-beta.2", - "@getpaseo/relay": "0.11.0-beta.2", + "@getpaseo/client": "0.11.0-beta.3", + "@getpaseo/highlight": "0.11.0-beta.3", + "@getpaseo/plugin": "0.11.0-beta.3", + "@getpaseo/protocol": "0.11.0-beta.3", + "@getpaseo/relay": "0.11.0-beta.3", "@isaacs/ttlcache": "^2.1.4", "@modelcontextprotocol/sdk": "1.29.0", "@opencode-ai/sdk": "1.14.46", @@ -40131,7 +40131,7 @@ }, "packages/website": { "name": "@getpaseo/website", - "version": "0.11.0-beta.2", + "version": "0.11.0-beta.3", "dependencies": { "@cloudflare/vite-plugin": "^1.29.1", "@cloudflare/workers-types": "^4.20260317.1", @@ -41736,7 +41736,7 @@ }, "plugins": { "name": "@getpaseo/builtin-plugins", - "version": "0.11.0-beta.2", + "version": "0.11.0-beta.3", "dependencies": { "@getpaseo/plugin": "*", "zod": "^4.4.3" diff --git a/package.json b/package.json index 50687c70fb..f27be7301c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "paseo", - "version": "0.11.0-beta.2", + "version": "0.11.0-beta.3", "private": true, "description": "Paseo: voice-controlled development environment for local AI coding agents", "keywords": [ diff --git a/packages/app/package.json b/packages/app/package.json index 31a314a419..2d6f308206 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/app", - "version": "0.11.0-beta.2", + "version": "0.11.0-beta.3", "private": true, "main": "index.ts", "scripts": { diff --git a/packages/cli/package.json b/packages/cli/package.json index a306be2a73..3ae9ccb2cf 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/cli", - "version": "0.11.0-beta.2", + "version": "0.11.0-beta.3", "description": "Paseo CLI - control your AI coding agents from the command line", "bin": { "paseo": "bin/paseo" @@ -28,9 +28,9 @@ }, "dependencies": { "@clack/prompts": "^1.0.0", - "@getpaseo/client": "0.11.0-beta.2", - "@getpaseo/protocol": "0.11.0-beta.2", - "@getpaseo/server": "0.11.0-beta.2", + "@getpaseo/client": "0.11.0-beta.3", + "@getpaseo/protocol": "0.11.0-beta.3", + "@getpaseo/server": "0.11.0-beta.3", "chalk": "^5.3.0", "commander": "^12.0.0", "mime-types": "^2.1.35", diff --git a/packages/client/package.json b/packages/client/package.json index 3adf29e33f..47fd164b08 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/client", - "version": "0.11.0-beta.2", + "version": "0.11.0-beta.3", "description": "Paseo client SDK package", "files": [ "dist", @@ -39,8 +39,8 @@ "test": "vitest run" }, "dependencies": { - "@getpaseo/protocol": "0.11.0-beta.2", - "@getpaseo/relay": "0.11.0-beta.2", + "@getpaseo/protocol": "0.11.0-beta.3", + "@getpaseo/relay": "0.11.0-beta.3", "zod": "^4.4.3" }, "devDependencies": { diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 74b481450f..a756b08ce7 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/desktop", - "version": "0.11.0-beta.2", + "version": "0.11.0-beta.3", "private": true, "description": "Paseo desktop app (Electron wrapper)", "homepage": "https://paseo.sh", diff --git a/packages/expo-two-way-audio/package.json b/packages/expo-two-way-audio/package.json index dae98ae5a4..41f4d1f0e9 100644 --- a/packages/expo-two-way-audio/package.json +++ b/packages/expo-two-way-audio/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/expo-two-way-audio", - "version": "0.11.0-beta.2", + "version": "0.11.0-beta.3", "description": "Native module for two way audio streaming", "keywords": [ "ExpoTwoWayAudio", diff --git a/packages/highlight/package.json b/packages/highlight/package.json index 8efa5ba85b..99624f726f 100644 --- a/packages/highlight/package.json +++ b/packages/highlight/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/highlight", - "version": "0.11.0-beta.2", + "version": "0.11.0-beta.3", "files": [ "dist", "!dist/**/*.map", diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 6cf9c32605..efb069a90a 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/plugin", - "version": "0.11.0-beta.2", + "version": "0.11.0-beta.3", "description": "Paseo plugin SDK package", "files": [ "dist", @@ -70,8 +70,8 @@ "use-sync-external-store": "^1.6.0" }, "devDependencies": { - "@getpaseo/client": "0.11.0-beta.2", - "@getpaseo/protocol": "0.11.0-beta.2", + "@getpaseo/client": "0.11.0-beta.3", + "@getpaseo/protocol": "0.11.0-beta.3", "@types/node": "^20.9.0", "@types/react": "~19.2.0", "react": "19.1.0", @@ -81,8 +81,8 @@ "zod": "^4.4.3" }, "peerDependencies": { - "@getpaseo/client": "0.11.0-beta.2", - "@getpaseo/protocol": "0.11.0-beta.2", + "@getpaseo/client": "0.11.0-beta.3", + "@getpaseo/protocol": "0.11.0-beta.3", "react": "~19.1.0", "react-native": ">=0.81.5", "zod": "^4.4.3" diff --git a/packages/protocol/package.json b/packages/protocol/package.json index ea5a112cb0..eb5ad255d4 100644 --- a/packages/protocol/package.json +++ b/packages/protocol/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/protocol", - "version": "0.11.0-beta.2", + "version": "0.11.0-beta.3", "description": "Paseo shared protocol schemas and wire types", "files": [ "dist", diff --git a/packages/relay/package.json b/packages/relay/package.json index 8b6e7d71a9..8e31759e57 100644 --- a/packages/relay/package.json +++ b/packages/relay/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/relay", - "version": "0.11.0-beta.2", + "version": "0.11.0-beta.3", "description": "Paseo relay for bridging daemon and client connections", "files": [ "dist", diff --git a/packages/server/package.json b/packages/server/package.json index 1de8aa483b..8218960c6a 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/server", - "version": "0.11.0-beta.2", + "version": "0.11.0-beta.3", "description": "Paseo backend server", "files": [ "dist/server", @@ -107,11 +107,11 @@ "@agentclientprotocol/sdk": "^0.17.1", "@anthropic-ai/claude-agent-sdk": "0.3.246", "@anthropic-ai/sdk": "^0.104.2", - "@getpaseo/client": "0.11.0-beta.2", - "@getpaseo/highlight": "0.11.0-beta.2", - "@getpaseo/plugin": "0.11.0-beta.2", - "@getpaseo/protocol": "0.11.0-beta.2", - "@getpaseo/relay": "0.11.0-beta.2", + "@getpaseo/client": "0.11.0-beta.3", + "@getpaseo/highlight": "0.11.0-beta.3", + "@getpaseo/plugin": "0.11.0-beta.3", + "@getpaseo/protocol": "0.11.0-beta.3", + "@getpaseo/relay": "0.11.0-beta.3", "@isaacs/ttlcache": "^2.1.4", "@modelcontextprotocol/sdk": "1.29.0", "@opencode-ai/sdk": "1.14.46", diff --git a/packages/website/package.json b/packages/website/package.json index e6f702b3ea..dcc25b32fa 100644 --- a/packages/website/package.json +++ b/packages/website/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/website", - "version": "0.11.0-beta.2", + "version": "0.11.0-beta.3", "private": true, "type": "module", "scripts": { diff --git a/plugins/package.json b/plugins/package.json index e9f3cb1ed6..b650ffa697 100644 --- a/plugins/package.json +++ b/plugins/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/builtin-plugins", - "version": "0.11.0-beta.2", + "version": "0.11.0-beta.3", "private": true, "type": "module", "scripts": { From bd3986d964b3ff528a8adf045a062dde10616934 Mon Sep 17 00:00:00 2001 From: "paseo-ai[bot]" <266920839+paseo-ai[bot]@users.noreply.github.com> Date: Fri, 2 Oct 2026 00:25:11 +0000 Subject: [PATCH 055/100] fix: update lockfile signatures and Nix hash [skip ci] --- nix/npm-deps.hash | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nix/npm-deps.hash b/nix/npm-deps.hash index 9ef4e45e82..37d5b2af73 100644 --- a/nix/npm-deps.hash +++ b/nix/npm-deps.hash @@ -1 +1 @@ -sha256-UZ9fPdCIJzghuxPXK9Kzt5fJmjyVwfaWsGiwXAh/zZI= +sha256-4X3h5SM6xUr3kpJTPX+v3ABacDz7fS2VbJnN3/f0bkk= From 6c985769beb689aa2b95c4a5955a3383d2c015a9 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Fri, 2 Oct 2026 10:10:33 +0100 Subject: [PATCH 056/100] test(app): tolerate footer icon subpixel rounding (#5898) Chromium bounding boxes differ by millionths of a pixel for equal-width footer buttons. Allow less than 0.01px in width and grouped-gap comparisons while preserving alignment and Help/Settings separation checks. --- packages/app/e2e/support/helpers/sidebar-nav-settings.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/app/e2e/support/helpers/sidebar-nav-settings.ts b/packages/app/e2e/support/helpers/sidebar-nav-settings.ts index c3ed13ea67..a8c64f712e 100644 --- a/packages/app/e2e/support/helpers/sidebar-nav-settings.ts +++ b/packages/app/e2e/support/helpers/sidebar-nav-settings.ts @@ -225,6 +225,9 @@ const FOOTER_ICON_TEST_IDS = [ "sidebar-settings", ]; +// Browser layout boxes include floating-point rounding, even for whole-pixel styles. +const FOOTER_GEOMETRY_TOLERANCE = 0.01; + /** * One line of same-size icons: Add project, Usage and Hosts together on the left, Help and * Settings together at the end. @@ -240,10 +243,12 @@ export async function expectFooterIconRow(page: Page): Promise { const [first] = boxes; for (const box of boxes) { expect(Math.abs(box.y + box.height / 2 - first!.y - first!.height / 2)).toBeLessThan(2); - expect(box.width).toBe(first!.width); + expect(Math.abs(box.width - first!.width)).toBeLessThan(FOOTER_GEOMETRY_TOLERANCE); } const gaps = boxes.slice(1).map((box, index) => box.x - (boxes[index]!.x + boxes[index]!.width)); - expect(gaps).toEqual([0, 0, expect.any(Number), 0]); + for (const index of [0, 1, 3]) { + expect(Math.abs(gaps[index]!)).toBeLessThan(FOOTER_GEOMETRY_TOLERANCE); + } expect(gaps[2]).toBeGreaterThan(first!.width); } From 73d25317a433caa60504dd11fcee05f6c932b6cc Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Fri, 2 Oct 2026 11:18:13 +0100 Subject: [PATCH 057/100] fix(usage): derive window names and unify pin selection (#5903) * fix(usage): derive window names and unify pin selection Quota slots do not identify a period. Derive numeric windows and IDs from reported duration and quota scope; keep unknown lengths neutral. Resolve source defaults and explicit pins through one model, with a persisted empty selection distinct from defaults. * fix(plugins): declare the duration helper host requirement * fix(usage): verify persisted selections at the storage boundary --- docs/providers.md | 4 +- packages/app/e2e/browser/usage-screen.spec.ts | 71 +++++++-- .../e2e/browser/usage-sidebar-item.spec.ts | 29 ++-- .../app/e2e/support/helpers/usage-login.ts | 74 +++++++++- .../src/hooks/use-settings/storage.test.ts | 23 ++- packages/app/src/usage/display.ts | 19 ++- packages/app/src/usage/host-usage-section.tsx | 2 +- packages/app/src/usage/pinned.test.ts | 17 ++- packages/app/src/usage/pinned.ts | 54 +++---- packages/app/src/usage/preferences.test.ts | 109 +++++++++++--- packages/app/src/usage/preferences.ts | 70 +++++++-- packages/plugin/src/server/usage.test.ts | 21 +++ packages/plugin/src/server/usage.ts | 49 +++++++ .../usage-source-directory/index.server.ts | 9 +- .../usage-source/index.server.ts | 9 +- .../server/plugins/usage-sources.e2e.test.ts | 7 + plugins/codex-usage-source/paseo-plugin.json | 2 +- .../codex-usage-source/server/usage.test.ts | 137 ++++++++++++++++-- plugins/codex-usage-source/server/usage.ts | 97 +++++++++---- .../grok-usage-source/server/usage.test.ts | 43 ++++++ plugins/grok-usage-source/server/usage.ts | 26 ++-- .../kimi-usage-source/server/usage.test.ts | 40 ++++- plugins/kimi-usage-source/server/usage.ts | 20 ++- plugins/muse-provider/paseo-plugin.json | 2 +- plugins/muse-provider/server/usage.ts | 8 +- plugins/muse-provider/test/provider.test.ts | 24 ++- public-docs/plugins/reference.md | 21 +++ 27 files changed, 815 insertions(+), 172 deletions(-) diff --git a/docs/providers.md b/docs/providers.md index 9aa2fe8bf6..47c39fd1a8 100644 --- a/docs/providers.md +++ b/docs/providers.md @@ -188,7 +188,9 @@ promise for completion: equal results, including equal discovery timestamps, emi ## Usage sources See the [public usage source reference](../public-docs/plugins/reference.md#usage-sources) for the -contract, account identity, login fallback, and read-only credential rules. +contract, account and window identity, provider-derived period names, login fallback, and +read-only credential rules. Usage adapters own the interpretation of provider fields; the app +renders their names and resolves pins without provider-specific duration guesses. --- diff --git a/packages/app/e2e/browser/usage-screen.spec.ts b/packages/app/e2e/browser/usage-screen.spec.ts index 71f3dfd8cc..48625eea4e 100644 --- a/packages/app/e2e/browser/usage-screen.spec.ts +++ b/packages/app/e2e/browser/usage-screen.spec.ts @@ -15,6 +15,8 @@ import { } from "../support/helpers/usage-reports"; import { installLoginUsage, + installCodexWindowUsage, + expectCodexReportedWindows, openUsage, refreshLoginUsage, hoverUsageWindow, @@ -312,29 +314,26 @@ test("expired login refreshes to windows with visible pin toggles", async ({ pag ).toHaveCount(0); await expectPinnedUsage(page, ["31% 5h", "54% wk"]); const row = page.getByRole("checkbox", { name: /^Pin Claude Weekly, / }); - await expect(row.getByTestId("usage-pin-glyph-unpinned")).toHaveCSS("opacity", "0"); + await expect(row).toBeChecked(); + await expect(row.getByTestId("usage-pin-glyph-pinned")).toHaveCSS("opacity", "0"); await hoverUsageWindow(page, "Weekly"); await expect(page.getByText("Pin", { exact: true })).toBeVisible(); - await expect(row.getByTestId("usage-pin-glyph-unpinned")).toHaveCSS("opacity", "1"); - await expect(row.getByTestId("usage-pin-glyph-unpinned").locator("svg")).toHaveAttribute( - "fill", - "none", - ); - await qaScreenshot(page, "usage-pin-hover"); - await togglePin(page.getByTestId("usage-report-login-journey:account"), "Claude", "Weekly"); await expect(row.getByTestId("usage-pin-glyph-pinned")).toHaveCSS("opacity", "1"); await expect(row.getByTestId("usage-pin-glyph-pinned").locator("svg")).not.toHaveAttribute( "fill", "none", ); - await expectPinnedUsage(page, ["54% wk"]); - await qaScreenshot(page, "usage-pin-selected"); + await qaScreenshot(page, "usage-pin-hover"); await togglePin(page.getByTestId("usage-report-login-journey:account"), "Claude", "Weekly"); await expect(row.getByTestId("usage-pin-glyph-unpinned")).toHaveCSS("opacity", "1"); await expect(row.getByTestId("usage-pin-glyph-unpinned").locator("svg")).toHaveAttribute( "fill", "none", ); + await expectPinnedUsage(page, ["31% 5h"]); + await qaScreenshot(page, "usage-pin-unselected"); + await togglePin(page.getByTestId("usage-report-login-journey:account"), "Claude", "Weekly"); + await expect(row.getByTestId("usage-pin-glyph-pinned")).toHaveCSS("opacity", "1"); await expectPinnedUsage(page, ["31% 5h", "54% wk"]); } finally { await fixture.cleanup(); @@ -348,11 +347,57 @@ test("compact usage rows always show the pin glyph", async ({ page }) => { await gotoAppShell(page); await openUsage(page); const card = page.getByTestId("usage-report-login-journey:account"); - await expect(card.getByTestId("usage-pin-glyph-unpinned")).toHaveCount(2); - await expect(card.getByTestId("usage-pin-glyph-unpinned").nth(0)).toHaveCSS("opacity", "1"); - await expect(card.getByTestId("usage-pin-glyph-unpinned").nth(1)).toHaveCSS("opacity", "1"); + await expect(card.getByTestId("usage-pin-glyph-pinned")).toHaveCount(2); + await expect(card.getByTestId("usage-pin-glyph-pinned").nth(0)).toHaveCSS("opacity", "1"); + await expect(card.getByTestId("usage-pin-glyph-pinned").nth(1)).toHaveCSS("opacity", "1"); await qaScreenshot(page, "usage-pin-compact"); } finally { await fixture.cleanup(); } }); + +for (const shape of ["seven-day-only", "Spark"] as const) { + test(`Codex ${shape} windows use provider durations on the card and sidebar`, async ({ + page, + }) => { + const reset = Math.floor(Date.now() / 1000) + 5 * 86400; + const source = await installCodexWindowUsage({ + rate_limit: { + primary_window: { used_percent: 11, limit_window_seconds: 604800, reset_at: reset }, + secondary_window: null, + }, + additional_rate_limits: + shape === "Spark" + ? [ + { + limit_name: "GPT-5.3-Codex-Spark", + metered_feature: "codex_bengalfox", + rate_limit: { + primary_window: { + used_percent: 0, + limit_window_seconds: 18000, + reset_at: Math.floor(Date.now() / 1000) + 3600, + }, + secondary_window: { + used_percent: 0, + limit_window_seconds: 604800, + reset_at: reset, + }, + }, + }, + ] + : [], + }); + try { + await page.setViewportSize({ width: 1440, height: 900 }); + await gotoAppShell(page); + await expect(usageItem(page)).toBeVisible(); + await usageItem(page).click(); + await qaScreenshot(page, `codex-${shape}-card-and-summary`); + await expectCodexReportedWindows(page, shape); + await qaScreenshot(page, `codex-${shape}-pins`); + } finally { + await source.cleanup(); + } + }); +} diff --git a/packages/app/e2e/browser/usage-sidebar-item.spec.ts b/packages/app/e2e/browser/usage-sidebar-item.spec.ts index 8a1d4bb7f0..902435a475 100644 --- a/packages/app/e2e/browser/usage-sidebar-item.spec.ts +++ b/packages/app/e2e/browser/usage-sidebar-item.spec.ts @@ -114,6 +114,11 @@ test.describe("Usage item", () => { await gotoAppShell(page); await usageItem(page).click(); await expectOnUsageScreen(page); + await qaScreenshot(page, "default-pins-card"); + await expect(pinRow(screen, "Claude", "Session")).toBeChecked(); + await expect(pinRow(screen, "Claude", "Weekly")).toBeChecked(); + await expect(pinRow(screen, "Codex", "Session")).toBeChecked(); + await expect(pinRow(screen, "Codex", "Weekly")).toBeChecked(); }); await test.step("pinning Claude 5-hour and Codex weekly shows both in the Usage item", async () => { @@ -122,9 +127,13 @@ test.describe("Usage item", () => { await expect(page.locator('[data-testid="usage-host-filter-trigger"]:visible')).toHaveCount( 0, ); - await togglePin(screen, "Claude", "Session"); - await expectPinnedUsage(page, ["31% 5h"]); - await togglePin(screen, "Codex", "Weekly"); + await togglePin(screen, "Claude", "Weekly"); + await expectPinnedUsage(page, ["31% 5h", "7% 5h", "12% wk"]); + await expect(pinRow(screen, "Claude", "Session")).toBeChecked(); + await expect(pinRow(screen, "Codex", "Session")).toBeChecked(); + await expect(pinRow(screen, "Codex", "Weekly")).toBeChecked(); + await qaScreenshot(page, "default-pins-after-one-toggle"); + await togglePin(screen, "Codex", "Session"); await expectPinnedUsage(page, ["31% 5h", "12% wk"]); await expect(usageItem(page)).not.toHaveText("Usage"); await qaScreenshot(page, "desktop-footer-pins", { kind: "footer" }); @@ -226,16 +235,18 @@ test.describe("Usage item", () => { await expect(screen.getByText("Claude", { exact: true })).toBeVisible({ timeout: 10_000 }); }); - await test.step("unpinning both brings back default windows, and that survives a reload", async () => { + await test.step("unpinning everything stays empty after a reload", async () => { await togglePin(screen, "Claude", "Session"); await togglePin(screen, "Codex", "Weekly"); - await expectPinnedUsage(page, ["69% 5h", "46% wk", "93% 5h", "88% wk"]); + await expectNoUsageItem(page); await page.reload(); await expect(screen.getByText("88% left")).toBeVisible({ timeout: 10_000 }); - await expectPinnedUsage(page, ["69% 5h", "46% wk", "93% 5h", "88% wk"]); - await gotoAppShell(page); - await usageItem(page).click(); - await expectOnUsageScreen(page); + await expectNoUsageItem(page); + await expect(pinRow(screen, "Claude", "Session")).not.toBeChecked(); + await expect(pinRow(screen, "Claude", "Weekly")).not.toBeChecked(); + await expect(pinRow(screen, "Codex", "Session")).not.toBeChecked(); + await expect(pinRow(screen, "Codex", "Weekly")).not.toBeChecked(); + await qaScreenshot(page, "explicit-empty-pins-after-reload"); }); await test.step("Settings > Sidebar lists the Usage item", async () => { diff --git a/packages/app/e2e/support/helpers/usage-login.ts b/packages/app/e2e/support/helpers/usage-login.ts index 5a2bef072d..4e83541b91 100644 --- a/packages/app/e2e/support/helpers/usage-login.ts +++ b/packages/app/e2e/support/helpers/usage-login.ts @@ -1,8 +1,9 @@ -import { mkdtemp, rm, writeFile } from "node:fs/promises"; +import { cp, mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; import type { UsageReport } from "@getpaseo/protocol/messages"; -import type { Page } from "@playwright/test"; +import { expect, type Page } from "@playwright/test"; +import { expectPinnedUsage, pinRow, usageItem } from "./usage-sidebar-item"; import { connectNewWorkspaceDaemonClient } from "./new-workspace"; import { pluginRequirements } from "./plugin-fixture"; @@ -61,6 +62,73 @@ export async function refreshLoginUsage(page: Page) { export async function hoverUsageWindow(page: Page, window: string) { await page .getByRole("checkbox", { name: new RegExp(`^Pin Claude ${window}, `) }) - .getByTestId("usage-pin-glyph-unpinned") + .getByTestId(/usage-pin-glyph-/) .hover(); } + +/** Exercises the built-in Codex adapter through the real plugin loader and daemon transport. */ +export async function installCodexWindowUsage(payload: unknown) { + const directory = await mkdtemp(path.join(tmpdir(), "usage-codex-windows-")); + const client = await connectNewWorkspaceDaemonClient({ ownProjects: false }); + const previous = await client.getDaemonConfig(); + const cleanup = async () => { + try { + await client.removePlugin("codex-window-qa"); + await client.patchDaemonConfig({ pluginsEnabled: previous.config.pluginsEnabled ?? false }); + } finally { + await client.close(); + await rm(directory, { recursive: true, force: true }); + } + }; + try { + const source = path.resolve(__dirname, "../../../../../plugins/codex-usage-source"); + await cp(path.join(source, "server"), path.join(directory, "server"), { recursive: true }); + await cp(path.join(source, "shared"), path.join(directory, "shared"), { recursive: true }); + await writeFile( + path.join(directory, "auth.json"), + JSON.stringify({ tokens: { access_token: "fixture-codex" } }), + ); + await writeFile(path.join(directory, "payload.json"), JSON.stringify(payload)); + await writeFile( + path.join(directory, "paseo-plugin.json"), + JSON.stringify({ id: "codex-window-qa", requirements: pluginRequirements }), + ); + await writeFile( + path.join(directory, "index.server.ts"), + ` +import {readFile} from "node:fs/promises"; +import {fetchUsage, discover} from "./server/usage.js"; +import {inputSchema} from "./shared/input.js"; +export default function contribute(server) { + server.registerUsageSource({id: "codex-window-qa", label: "Codex", input: inputSchema, + discover: () => discover({home: ${JSON.stringify(directory)}, env: {CODEX_HOME: ${JSON.stringify(directory)}}}), + fetch: (input) => fetchUsage(input, async () => Response.json(JSON.parse(await readFile(${JSON.stringify(path.join(directory, "payload.json"))}, "utf8")))), + }); + return () => {}; +}`, + ); + await client.patchDaemonConfig({ pluginsEnabled: true }); + await client.installPluginSource({ source: directory }); + return { cleanup }; + } catch (error) { + await cleanup(); + throw error; + } +} + +export async function expectCodexReportedWindows(page: Page, shape: "seven-day-only" | "Spark") { + await expectPinnedUsage(page, ["11% wk"]); + await expect(pinRow(page.locator("body"), "Codex", "Weekly")).toBeChecked(); + await expect(page.getByText("Session", { exact: true })).toHaveCount(0); + if (shape === "Spark") { + const fiveHour = pinRow(page.locator("body"), "Codex", "GPT-5.3-Codex-Spark · 5-hour"); + const weekly = pinRow(page.locator("body"), "Codex", "GPT-5.3-Codex-Spark · Weekly"); + await expect(fiveHour).not.toBeChecked(); + await expect(weekly).not.toBeChecked(); + await fiveHour.click(); + await expect(fiveHour).toBeChecked(); + await expect(usageItem(page)).toHaveAccessibleName( + /Codex Weekly 11% used, Codex GPT-5.3-Codex-Spark · 5-hour 0% used/, + ); + } +} diff --git a/packages/app/src/hooks/use-settings/storage.test.ts b/packages/app/src/hooks/use-settings/storage.test.ts index 9761256278..d396f26d9a 100644 --- a/packages/app/src/hooks/use-settings/storage.test.ts +++ b/packages/app/src/hooks/use-settings/storage.test.ts @@ -263,10 +263,10 @@ describe("loadAppSettingsFromStorage", () => { ]); }); - it("loads usage preferences with pins in order and defaults a fresh device to none", async () => { + it("loads legacy usage pins in order and defaults a fresh device to source defaults", async () => { expect((await loadAppSettingsFromStorage(makeDeps())).usage).toEqual({ displayAs: "used", - pinned: [], + pins: null, serverId: null, }); @@ -287,7 +287,7 @@ describe("loadAppSettingsFromStorage", () => { expect((await loadAppSettingsFromStorage(deps)).usage).toEqual({ displayAs: "remaining", - pinned: [ + pins: [ { sourceId: "codex", windowId: "weekly" }, { sourceId: "claude", windowId: "five-hour" }, ], @@ -295,6 +295,21 @@ describe("loadAppSettingsFromStorage", () => { }); }); + it("loads an explicitly empty usage selection without restoring defaults", async () => { + const deps = makeDeps({ + storage: createInMemoryKeyValueStorage({ + [APP_SETTINGS_KEY]: JSON.stringify({ + usage: { displayAs: "remaining", pins: [], serverId: "server" }, + }), + }), + }); + expect((await loadAppSettingsFromStorage(deps)).usage).toEqual({ + displayAs: "remaining", + pins: [], + serverId: "server", + }); + }); + it("keeps valid usage preferences when one field is malformed", async () => { const deps = makeDeps({ storage: createInMemoryKeyValueStorage({ @@ -310,7 +325,7 @@ describe("loadAppSettingsFromStorage", () => { expect((await loadAppSettingsFromStorage(deps)).usage).toEqual({ displayAs: "used", - pinned: [{ sourceId: "claude", windowId: "weekly" }], + pins: [{ sourceId: "claude", windowId: "weekly" }], serverId: null, }); }); diff --git a/packages/app/src/usage/display.ts b/packages/app/src/usage/display.ts index 09fdb3d8ea..cfacf8d738 100644 --- a/packages/app/src/usage/display.ts +++ b/packages/app/src/usage/display.ts @@ -1,5 +1,7 @@ import { useCallback, useMemo } from "react"; import { useAppSettings } from "@/hooks/use-settings"; +import { useUsageHostId } from "./hosts"; +import { useUsageHostReports } from "./queries"; import { isUsagePinned, setUsageDisplayAs, @@ -18,9 +20,14 @@ export interface UsageDisplay { } /** The device's usage preferences, and the commands that change them. */ -export function useUsagePreferences(): { preferences: UsagePreferences; display: UsageDisplay } { +export function useUsagePreferences(serverId?: string): { + preferences: UsagePreferences; + display: UsageDisplay; +} { const { settings, updateSettings } = useAppSettings(); const preferences = settings.usage; + const defaultServerId = useUsageHostId(); + const reports = useUsageHostReports(serverId ?? defaultServerId); const setDisplayAs = useCallback( (displayAs: UsageDisplayAs) => { void updateSettings((current) => ({ usage: setUsageDisplayAs(current.usage, displayAs) })); @@ -29,18 +36,20 @@ export function useUsagePreferences(): { preferences: UsagePreferences; display: ); const togglePin = useCallback( (pin: UsagePin) => { - void updateSettings((current) => ({ usage: toggleUsagePin(current.usage, pin) })); + void updateSettings((current) => ({ + usage: toggleUsagePin({ preferences: current.usage, pin, reports }), + })); }, - [updateSettings], + [updateSettings, reports], ); const display = useMemo( () => ({ displayAs: preferences.displayAs, setDisplayAs, - isPinned: (pin) => isUsagePinned(preferences, pin), + isPinned: (pin) => isUsagePinned(preferences, pin, reports), togglePin, }), - [preferences, setDisplayAs, togglePin], + [preferences, reports, setDisplayAs, togglePin], ); return { preferences, display }; } diff --git a/packages/app/src/usage/host-usage-section.tsx b/packages/app/src/usage/host-usage-section.tsx index f3cb0af3b0..927d32e7d8 100644 --- a/packages/app/src/usage/host-usage-section.tsx +++ b/packages/app/src/usage/host-usage-section.tsx @@ -6,7 +6,7 @@ import { UsageSection } from "./usage-section"; /** A host's usage reports, for its settings page. */ export function HostUsageSection({ serverId }: { serverId: string }) { const { view, refresh } = useHostUsage(serverId); - const { display } = useUsagePreferences(); + const { display } = useUsagePreferences(serverId); return ( { @@ -73,7 +73,7 @@ describe("choosePinnedUsageLayout", () => { }); describe("resolvePinnedUsage", () => { - it("defaults to the first window with a percent for each source account", () => { + it("defaults to source-wide pins collected from each account", () => { const work = report({ sourceId: "claude", sourceLabel: "Claude", @@ -89,12 +89,13 @@ describe("resolvePinnedUsage", () => { windows: [{ id: "empty", label: "Empty" }], }); expect( - windows([claude, codex, work, empty], preferences([])).map((item) => [ + windows([claude, codex, work, empty], preferences(null)).map((item) => [ item.key, item.percentText, ]), ).toEqual([ ["claude:default/five-hour", "31%"], + ["claude:default/weekly", "80%"], ["claude:work/weekly", "90%"], ["codex:default/weekly", "12%"], ]); @@ -107,7 +108,7 @@ describe("resolvePinnedUsage", () => { (item) => item.key, ), ).toEqual(["claude:default/weekly"]); - expect(windows(reports, preferences([])).map((item) => item.key)).toEqual([ + expect(windows(reports, preferences(null)).map((item) => item.key)).toEqual([ "claude:default/five-hour", "codex:default/weekly", ]); @@ -176,7 +177,7 @@ describe("resolvePinnedUsage", () => { ], }); - expect(windows([marked, codex], preferences([])).map((item) => item.key)).toEqual([ + expect(windows([marked, codex], preferences(null)).map((item) => item.key)).toEqual([ "claude:default/five-hour", "claude:default/weekly", "codex:default/weekly", @@ -189,7 +190,7 @@ describe("resolvePinnedUsage", () => { sourceLabel: "OpenCode Go", windows: [{ id: "rolling", label: "Rolling", shortLabel: "", usedPct: 21 }], }); - const items = windows([opencode], preferences([])); + const items = windows([opencode], preferences(null)); expect(items.map((item) => item.shortLabel)).toEqual([""]); }); @@ -268,5 +269,5 @@ it("keeps unavailable and error reports out of the sidebar summary", () => { ...codex, report: { status: "error", error: "Store deleted" }, }; - expect(resolvePinnedUsage([expired, failed], preferences([]))).toEqual([]); + expect(resolvePinnedUsage([expired, failed], preferences(null))).toEqual([]); }); diff --git a/packages/app/src/usage/pinned.ts b/packages/app/src/usage/pinned.ts index f05f64cdbc..547b2c3e48 100644 --- a/packages/app/src/usage/pinned.ts +++ b/packages/app/src/usage/pinned.ts @@ -1,8 +1,8 @@ import { formatPct } from "./format"; import { displayPercent } from "./model"; -import type { UsagePreferences } from "./preferences"; +import { effectiveUsagePins, type UsagePreferences } from "./preferences"; import { windowTone } from "./tone"; -import type { UsageReportEntry, UsageTone, UsageWindow } from "./types"; +import type { UsageReportEntry, UsageTone } from "./types"; /** One summary window of one account, as the sidebar Usage item shows it. */ export interface PinnedUsageWindow { @@ -61,24 +61,6 @@ function groupBySource(reports: readonly UsageReportEntry[]): UsageReportEntry[] return sourceIds.flatMap((sourceId) => reports.filter((entry) => entry.sourceId === sourceId)); } -/** - * Pins replace defaults. Without pins, an account shows the windows its source marks as summary, - * or its first window with a percent when the source marks none. - */ -function summaryWindows(entry: UsageReportEntry, preferences: UsagePreferences): UsageWindow[] { - if (entry.report.status !== "available") return []; - const withPercent = entry.report.windows.filter( - (window) => displayPercent(window, preferences.displayAs) !== null, - ); - if (preferences.pinned.length === 0) { - const marked = withPercent.filter((window) => window.summary); - return marked.length > 0 ? marked : withPercent.slice(0, 1); - } - return withPercent.filter((window) => - preferences.pinned.some((pin) => pin.sourceId === entry.sourceId && pin.windowId === window.id), - ); -} - /** * The sidebar Usage item's summary: one group per account, grouped by source, each with its * windows in its report's order. Accounts with nothing to show are left out. @@ -87,20 +69,28 @@ export function resolvePinnedUsage( reports: readonly UsageReportEntry[], preferences: UsagePreferences, ): PinnedUsageSource[] { + const pins = effectiveUsagePins(preferences, reports); const meaning = preferences.displayAs === "remaining" ? "left" : "used"; return groupBySource(reports).flatMap((entry) => { - const windows = summaryWindows(entry, preferences).map((window) => { - const percent = displayPercent(window, preferences.displayAs) ?? 0; - const percentText = formatPct(percent); - return { - key: `${entry.id}/${window.id}`, - label: `${describe(entry, window.label)} ${percentText} ${meaning}`, - shortLabel: window.shortLabel ?? window.label, - percent, - percentText, - tone: windowTone(window), - }; - }); + if (entry.report.status !== "available") return []; + const windows = entry.report.windows + .filter( + (window) => + displayPercent(window, preferences.displayAs) !== null && + pins.some((pin) => pin.sourceId === entry.sourceId && pin.windowId === window.id), + ) + .map((window) => { + const percent = displayPercent(window, preferences.displayAs) ?? 0; + const percentText = formatPct(percent); + return { + key: `${entry.id}/${window.id}`, + label: `${describe(entry, window.label)} ${percentText} ${meaning}`, + shortLabel: window.shortLabel ?? window.label, + percent, + percentText, + tone: windowTone(window), + }; + }); if (windows.length === 0) return []; return [{ key: entry.id, icon: entry.icon ?? null, windows }]; }); diff --git a/packages/app/src/usage/preferences.test.ts b/packages/app/src/usage/preferences.test.ts index 391ec05cd3..43dccc466d 100644 --- a/packages/app/src/usage/preferences.test.ts +++ b/packages/app/src/usage/preferences.test.ts @@ -13,45 +13,118 @@ const codexWeekly = { sourceId: "codex", windowId: "weekly" }; describe("usage preferences", () => { it("pins windows in the order the user pins them", () => { - const pinned = toggleUsagePin( - toggleUsagePin(DEFAULT_USAGE_PREFERENCES, codexWeekly), - claudeFiveHour, - ); + const pinned = toggleUsagePin({ + preferences: toggleUsagePin({ + preferences: DEFAULT_USAGE_PREFERENCES, + pin: codexWeekly, + reports: [], + }), + pin: claudeFiveHour, + reports: [], + }); - expect(pinned.pinned).toEqual([codexWeekly, claudeFiveHour]); - expect(isUsagePinned(pinned, { sourceId: "codex", windowId: "weekly" })).toBe(true); - expect(isUsagePinned(pinned, { sourceId: "codex", windowId: "five-hour" })).toBe(false); + expect(pinned.pins).toEqual([codexWeekly, claudeFiveHour]); + expect(isUsagePinned(pinned, { sourceId: "codex", windowId: "weekly" }, [])).toBe(true); + expect(isUsagePinned(pinned, { sourceId: "codex", windowId: "five-hour" }, [])).toBe(false); }); it("unpins a pinned window and keeps the others in order", () => { const three = [codexWeekly, claudeFiveHour, { sourceId: "claude", windowId: "weekly" }]; - const unpinned = toggleUsagePin( - { displayAs: "used", pinned: three, serverId: null }, - claudeFiveHour, - ); + const unpinned = toggleUsagePin({ + preferences: { displayAs: "used", pins: three, serverId: null }, + pin: claudeFiveHour, + reports: [], + }); - expect(unpinned.pinned).toEqual([codexWeekly, { sourceId: "claude", windowId: "weekly" }]); + expect(unpinned.pins).toEqual([codexWeekly, { sourceId: "claude", windowId: "weekly" }]); }); it("switches between used and remaining without touching pins", () => { - const preferences = { displayAs: "used" as const, pinned: [codexWeekly], serverId: null }; + const preferences = { displayAs: "used" as const, pins: [codexWeekly], serverId: null }; expect(setUsageDisplayAs(preferences, "remaining")).toEqual({ displayAs: "remaining", - pinned: [codexWeekly], + pins: [codexWeekly], serverId: null, }); }); it("remembers the picked host without touching pins", () => { - expect(setUsageHost({ ...DEFAULT_USAGE_PREFERENCES, pinned: [codexWeekly] }, "server")).toEqual( - { displayAs: "used", pinned: [codexWeekly], serverId: "server" }, - ); + expect(setUsageHost({ ...DEFAULT_USAGE_PREFERENCES, pins: [codexWeekly] }, "server")).toEqual({ + displayAs: "used", + pins: [codexWeekly], + serverId: "server", + }); }); it("reads preferences saved before hosts could be picked as no pick", () => { expect(UsagePreferencesSchema.parse({ displayAs: "remaining", pinned: [codexWeekly] })).toEqual( - { displayAs: "remaining", pinned: [codexWeekly], serverId: null }, + { displayAs: "remaining", pins: [codexWeekly], serverId: null }, ); }); }); + +it("default sidebar windows are pinned on cards and the first toggle removes only that row", () => { + const reports = [ + { + id: "claude:default", + sourceId: "claude", + sourceLabel: "Claude", + account: {}, + fetchedAt: "2026-10-02T00:00:00Z", + report: { + status: "available" as const, + windows: [ + { id: "five_hour", label: "5-hour", summary: true, usedPct: 31 }, + { id: "weekly", label: "Weekly", summary: true, usedPct: 54 }, + ], + }, + }, + { + id: "codex:default", + sourceId: "codex", + sourceLabel: "Codex", + account: {}, + fetchedAt: "2026-10-02T00:00:00Z", + report: { + status: "available" as const, + windows: [{ id: "five_hour", label: "5-hour", summary: true, usedPct: 7 }], + }, + }, + ]; + const pins = [ + { sourceId: "claude", windowId: "five_hour" }, + { sourceId: "claude", windowId: "weekly" }, + { sourceId: "codex", windowId: "five_hour" }, + ]; + for (const pin of pins) expect(isUsagePinned(DEFAULT_USAGE_PREFERENCES, pin, reports)).toBe(true); + const changed = toggleUsagePin({ + preferences: DEFAULT_USAGE_PREFERENCES, + pin: pins[1]!, + reports, + }); + expect(isUsagePinned(changed, pins[1]!, reports)).toBe(false); + expect(isUsagePinned(changed, pins[0]!, reports)).toBe(true); + expect(isUsagePinned(changed, pins[2]!, reports)).toBe(true); + const empty = toggleUsagePin({ + preferences: toggleUsagePin({ preferences: changed, pin: pins[0]!, reports }), + pin: pins[2]!, + reports, + }); + const restored = UsagePreferencesSchema.parse(JSON.parse(JSON.stringify(empty))); + for (const pin of pins) expect(isUsagePinned(restored, pin, reports)).toBe(false); +}); + +it("reads old selections without losing display or host and preserves an explicit empty selection", () => { + for (const [input, pins] of [ + [{ pinned: [] }, null], + [{ pinned: [codexWeekly] }, [codexWeekly]], + [{ pins: [], pinned: [codexWeekly] }, []], + [{ pins: null, pinned: [codexWeekly] }, null], + [{ pins: "bad", pinned: [codexWeekly] }, null], + ] as const) { + expect( + UsagePreferencesSchema.parse({ displayAs: "remaining", serverId: "host", ...input }), + ).toEqual({ displayAs: "remaining", serverId: "host", pins }); + } +}); diff --git a/packages/app/src/usage/preferences.ts b/packages/app/src/usage/preferences.ts index c25cf6e1c7..e082870af6 100644 --- a/packages/app/src/usage/preferences.ts +++ b/packages/app/src/usage/preferences.ts @@ -1,4 +1,6 @@ import { z } from "zod"; +import { displayPercent } from "./model"; +import type { UsageReportEntry } from "./types"; /** Whether percentages read as the share used or the share left. */ export type UsageDisplayAs = "used" | "remaining"; @@ -9,42 +11,86 @@ export interface UsagePin { windowId: string; } -/** Device-local. `pinned` is in display order. */ +/** Device-local. `pins` is in selection order. */ export interface UsagePreferences { displayAs: UsageDisplayAs; - pinned: UsagePin[]; + /** null follows source defaults; an array is the user's selection, including none. */ + pins: UsagePin[] | null; /** The host the user picked to show usage for; null until they pick one. */ serverId: string | null; } export const DEFAULT_USAGE_PREFERENCES: UsagePreferences = { displayAs: "used", - pinned: [], + pins: null, serverId: null, }; +const pinsSchema = z.array(z.object({ sourceId: z.string(), windowId: z.string() })); + export const UsagePreferencesSchema = z .object({ displayAs: z.enum(["used", "remaining"]).catch("used"), - pinned: z.array(z.object({ sourceId: z.string(), windowId: z.string() })).catch([]), + pins: pinsSchema.nullable().optional().catch(null), + // COMPAT(usagePinSelection): added in v0.11, remove after 2027-04-02. + // Old [] meant defaults. Read it only when the new selection is absent. + pinned: pinsSchema.optional().catch([]), serverId: z.string().nullable().catch(null), }) + .transform(({ displayAs, pins, pinned, serverId }): UsagePreferences => { + const legacyPins = pinned && pinned.length > 0 ? pinned : null; + return { displayAs, pins: pins === undefined ? legacyPins : pins, serverId }; + }) .catch(DEFAULT_USAGE_PREFERENCES); function samePin(a: UsagePin, b: UsagePin): boolean { return a.sourceId === b.sourceId && a.windowId === b.windowId; } -export function isUsagePinned(preferences: UsagePreferences, pin: UsagePin): boolean { - return preferences.pinned.some((pinned) => samePin(pinned, pin)); +/** The only owner of default and customized pin selection, shared by every usage surface. */ +export function effectiveUsagePins( + preferences: UsagePreferences, + reports: readonly UsageReportEntry[], +): UsagePin[] { + if (preferences.pins !== null) return preferences.pins; + const pins: UsagePin[] = []; + for (const entry of reports) { + if (entry.report.status !== "available") continue; + const withPercent = entry.report.windows.filter( + (window) => displayPercent(window, preferences.displayAs) !== null, + ); + const marked = withPercent.filter((window) => window.summary); + for (const window of marked.length > 0 ? marked : withPercent.slice(0, 1)) { + const pin = { sourceId: entry.sourceId, windowId: window.id }; + if (!pins.some((existing) => samePin(existing, pin))) pins.push(pin); + } + } + return pins; } -/** Unpins a pinned window; pins any other at the end, so the Usage item grows to the right. */ -export function toggleUsagePin(preferences: UsagePreferences, pin: UsagePin): UsagePreferences { - const pinned = isUsagePinned(preferences, pin) - ? preferences.pinned.filter((existing) => !samePin(existing, pin)) - : [...preferences.pinned, { sourceId: pin.sourceId, windowId: pin.windowId }]; - return { ...preferences, pinned }; +export function isUsagePinned( + preferences: UsagePreferences, + pin: UsagePin, + reports: readonly UsageReportEntry[], +): boolean { + return effectiveUsagePins(preferences, reports).some((pinned) => samePin(pinned, pin)); +} + +/** The first edit snapshots the displayed defaults before toggling one window. */ +export function toggleUsagePin({ + preferences, + pin, + reports, +}: { + preferences: UsagePreferences; + pin: UsagePin; + reports: readonly UsageReportEntry[]; +}): UsagePreferences { + const selected = effectiveUsagePins(preferences, reports); + const pins = selected.some((existing) => samePin(existing, pin)) + ? selected.filter((existing) => !samePin(existing, pin)) + : [...selected, { sourceId: pin.sourceId, windowId: pin.windowId }]; + return { ...preferences, pins }; } export function setUsageDisplayAs( diff --git a/packages/plugin/src/server/usage.test.ts b/packages/plugin/src/server/usage.test.ts index 1a8c5de939..b70e8d6d01 100644 --- a/packages/plugin/src/server/usage.test.ts +++ b/packages/plugin/src/server/usage.test.ts @@ -4,6 +4,7 @@ import { toneFromUsedPct, usedPctOf, windowFromUsedPct, + windowFromReportedDuration, } from "./usage.js"; test("shared usage tones preserve the app threshold contract", () => { @@ -84,3 +85,23 @@ describe("balanceToneFromRemaining", () => { expect(balanceToneFromRemaining(null)).toBe("default"); }); }); + +test.each([ + [18000, "five_hour", "5-hour", "5h"], + [604800, "weekly", "Weekly", "wk"], + [7200, "7200s", "2-hour", "2h"], + [86400, "86400s", "1-day", "1d"], + [90, "90s", "90-second", "90s"], + [null, "primary", "Primary limit", ""], +])( + "duration %s owns the window's identity and both names", + (durationSeconds, id, label, shortLabel) => { + expect( + windowFromReportedDuration({ + durationSeconds: durationSeconds as number | null, + unknown: { id: "primary", label: "Primary limit", shortLabel: "" }, + utilizationPct: 11, + }), + ).toMatchObject({ id, label, shortLabel, usedPct: 11 }); + }, +); diff --git a/packages/plugin/src/server/usage.ts b/packages/plugin/src/server/usage.ts index ebf00c3b2f..1b78f8fe12 100644 --- a/packages/plugin/src/server/usage.ts +++ b/packages/plugin/src/server/usage.ts @@ -96,6 +96,55 @@ export function windowFromUsedPct(input: { return window; } +/** + * Numeric provider windows have one identity and vocabulary, independent of response slots. + * Pass null when the provider omits the duration; reset countdowns are not window lengths. + * Named API fields (weekly, monthly, etc.) use windowFromUsedPct instead. + */ +export function windowFromReportedDuration(input: { + durationSeconds: number | null; + /** Stable quota identity and provider name for a model- or feature-scoped limit. */ + scope?: { id: string; label: string }; + /** Neutral identity and names when the provider does not report a positive duration. */ + unknown: { id: string; label: string; shortLabel: string }; + utilizationPct: number | null | undefined; + resetsAt?: string | null; + summary?: boolean; + tone?: UsageWindow["tone"]; +}): UsageWindow { + const duration = input.durationSeconds; + const name = + duration !== null && Number.isFinite(duration) && duration > 0 + ? durationWindowName(duration) + : input.unknown; + const scope = input.scope; + return windowFromUsedPct({ + id: scope ? `${scope.id}:${name.id}` : name.id, + label: scope ? `${scope.label} · ${name.label}` : name.label, + shortLabel: scope + ? `${scope.label}${name.shortLabel ? ` ${name.shortLabel}` : ""}` + : name.shortLabel, + utilizationPct: input.utilizationPct, + resetsAt: input.resetsAt, + summary: input.summary, + tone: input.tone, + }); +} + +function durationWindowName(seconds: number): { id: string; label: string; shortLabel: string } { + if (seconds === 604800) return { id: "weekly", label: "Weekly", shortLabel: "wk" }; + const id = seconds === 18000 ? "five_hour" : `${seconds}s`; + const units = [ + [86400, "day", "d"], + [3600, "hour", "h"], + [60, "minute", "m"], + [1, "second", "s"], + ] as const; + const unit = units.find(([size]) => seconds % size === 0) ?? units[units.length - 1]!; + const amount = seconds / unit[0]; + return { id, label: `${amount}-${unit[1]}`, shortLabel: `${amount}${unit[2]}` }; +} + /** * The tone scale for anything measured against a known limit, windows and balances alike. * diff --git a/packages/server/src/server/plugins/test-fixtures/usage-source-directory/index.server.ts b/packages/server/src/server/plugins/test-fixtures/usage-source-directory/index.server.ts index 0663e0070d..9a0f102a29 100644 --- a/packages/server/src/server/plugins/test-fixtures/usage-source-directory/index.server.ts +++ b/packages/server/src/server/plugins/test-fixtures/usage-source-directory/index.server.ts @@ -1,5 +1,6 @@ import type { PluginServerContext } from "@getpaseo/plugin/server"; import { z } from "zod"; +import { windowFromReportedDuration } from "@getpaseo/plugin/server/usage"; let fetches = 0; export default function contribute(server: PluginServerContext) { @@ -32,7 +33,13 @@ export default function contribute(server: PluginServerContext) { fetches++; return { status: "available", - windows: [{ id: "count", label: "Count", usedPct: fetches }], + windows: [ + windowFromReportedDuration({ + durationSeconds: 604800, + unknown: { id: "unknown", label: "Limit", shortLabel: "" }, + utilizationPct: fetches, + }), + ], }; }, }); diff --git a/packages/server/src/server/plugins/test-fixtures/usage-source/index.server.ts b/packages/server/src/server/plugins/test-fixtures/usage-source/index.server.ts index 12d7ced848..e42aa64edc 100644 --- a/packages/server/src/server/plugins/test-fixtures/usage-source/index.server.ts +++ b/packages/server/src/server/plugins/test-fixtures/usage-source/index.server.ts @@ -1,5 +1,6 @@ import type { PluginServerContext } from "@getpaseo/plugin/server"; import { z } from "zod"; +import { windowFromReportedDuration } from "@getpaseo/plugin/server/usage"; let fetches = 0; export default function contribute(server: PluginServerContext) { @@ -32,7 +33,13 @@ export default function contribute(server: PluginServerContext) { fetches++; return { status: "available", - windows: [{ id: "count", label: "Count", usedPct: fetches }], + windows: [ + windowFromReportedDuration({ + durationSeconds: 604800, + unknown: { id: "unknown", label: "Limit", shortLabel: "" }, + utilizationPct: fetches, + }), + ], }; }, }); diff --git a/packages/server/src/server/plugins/usage-sources.e2e.test.ts b/packages/server/src/server/plugins/usage-sources.e2e.test.ts index 0b2b3ca9a2..cff548adc5 100644 --- a/packages/server/src/server/plugins/usage-sources.e2e.test.ts +++ b/packages/server/src/server/plugins/usage-sources.e2e.test.ts @@ -57,6 +57,13 @@ test("lists built-in and subprocess usage; validates input and isolates fetch er await client.installDirectoryPlugin(subprocessDirectory, "fixture-directory"); const both = await client.listUsageReports({ forceRefresh: true }); expect(both.reports).toHaveLength(8); + for (const id of ["fixture:one", "fixture-directory:one"]) { + expect(both.reports.find((entry) => entry.id === id)?.report.windows[0]).toMatchObject({ + id: "weekly", + label: "Weekly", + shortLabel: "wk", + }); + } await client.patchDaemonConfig({ pluginsEnabled: false }); await expect.poll(async () => (await client.listUsageReports()).reports.length).toBe(4); } finally { diff --git a/plugins/codex-usage-source/paseo-plugin.json b/plugins/codex-usage-source/paseo-plugin.json index 64470099fb..f86baa137a 100644 --- a/plugins/codex-usage-source/paseo-plugin.json +++ b/plugins/codex-usage-source/paseo-plugin.json @@ -1 +1 @@ -{ "id": "codex-usage-source", "requirements": { "paseo": ">=0.9.2" } } +{ "id": "codex-usage-source", "requirements": { "paseo": ">=0.11.0" } } diff --git a/plugins/codex-usage-source/server/usage.test.ts b/plugins/codex-usage-source/server/usage.test.ts index c70066c886..e92c7ea2f2 100644 --- a/plugins/codex-usage-source/server/usage.test.ts +++ b/plugins/codex-usage-source/server/usage.test.ts @@ -39,7 +39,9 @@ function response(headers: HeadersInit, accountId?: string): Promise { new Response( JSON.stringify({ plan_type: "plus", - rate_limit: { primary_window: { used_percent: 30, reset_at: 1700000000 } }, + rate_limit: { + primary_window: { used_percent: 30, limit_window_seconds: 18000, reset_at: 1700000000 }, + }, }), { status: 200 }, ), @@ -62,7 +64,7 @@ test("explicit route reads Codex auth and preserves the usage request", async () expect(report).toMatchObject({ status: "available", planLabel: "plus", - windows: [{ id: "session", usedPct: 30 }], + windows: [{ id: "five_hour", usedPct: 30 }], }); } finally { await rm(home, { recursive: true, force: true }); @@ -95,8 +97,8 @@ test("coerces credit balance and marks a 96 percent window dangerous", async () new Response( JSON.stringify({ rate_limit: { - primary_window: { used_percent: 12 }, - secondary_window: { used_percent: 96 }, + primary_window: { used_percent: 12, limit_window_seconds: 18000 }, + secondary_window: { used_percent: 96, limit_window_seconds: 604800 }, }, credits: { balance: "0" }, }), @@ -105,7 +107,7 @@ test("coerces credit balance and marks a 96 percent window dangerous", async () ); expect(report.windows).toEqual( expect.arrayContaining([ - expect.objectContaining({ id: "session", tone: "ok" }), + expect.objectContaining({ id: "five_hour", tone: "ok" }), expect.objectContaining({ id: "weekly", tone: "danger" }), ]), ); @@ -121,8 +123,8 @@ test("summarizes the session and weekly windows by default, not code review", as new Response( JSON.stringify({ rate_limit: { - primary_window: { used_percent: 12 }, - secondary_window: { used_percent: 40 }, + primary_window: { used_percent: 12, limit_window_seconds: 18000 }, + secondary_window: { used_percent: 40, limit_window_seconds: 604800 }, }, code_review_rate_limit: { primary_window: { used_percent: 5 } }, }), @@ -130,9 +132,9 @@ test("summarizes the session and weekly windows by default, not code review", as ), ); expect(report.windows.map((window) => [window.id, window.summary ?? false])).toEqual([ - ["session", true], + ["five_hour", true], ["weekly", true], - ["code_review", false], + ["code_review:unknown_primary", false], ]); }); @@ -260,3 +262,120 @@ test.each([401, 403])("reports a Codex login rejected with HTTP %i", async (stat await fetchUsage(authInput(fixtureHome), async () => new Response(null, { status })), ).toEqual({ status: "unavailable", problem: { kind: "rejected", status, refreshedBy: "codex" } }); }); + +// Issue #4165: the primary slot contains the only weekly window. +test("names a seven-day-only primary window from its reported duration", async () => { + const report = await fetchUsage(authInput(fixtureHome), async () => + Response.json({ + rate_limit: { + primary_window: { used_percent: 1, limit_window_seconds: 604800 }, + secondary_window: null, + }, + }), + ); + expect(report).toMatchObject({ + status: "available", + windows: [{ id: "weekly", label: "Weekly", shortLabel: "wk", usedPct: 1 }], + }); +}); + +test("reports every named additional limit with its own duration identity", async () => { + const report = await fetchUsage(authInput(fixtureHome), async () => + Response.json({ + additional_rate_limits: [ + { + limit_name: "GPT-5.3-Codex-Spark", + metered_feature: "codex_bengalfox", + rate_limit: { + primary_window: { used_percent: 0, limit_window_seconds: 18000 }, + secondary_window: { used_percent: 0, limit_window_seconds: 604800 }, + }, + }, + { + limit_name: "Another limit", + metered_feature: "another", + rate_limit: { + primary_window: { used_percent: 4, limit_window_seconds: 7200 }, + }, + }, + ], + }), + ); + expect(report).toMatchObject({ + status: "available", + windows: [ + { + id: "limit:codex_bengalfox:five_hour", + label: "GPT-5.3-Codex-Spark · 5-hour", + shortLabel: "GPT-5.3-Codex-Spark 5h", + }, + { + id: "limit:codex_bengalfox:weekly", + label: "GPT-5.3-Codex-Spark · Weekly", + shortLabel: "GPT-5.3-Codex-Spark wk", + }, + { id: "limit:another:7200s", label: "Another limit · 2-hour" }, + ], + }); +}); + +test("a weekly pin means weekly after the provider moves it between slots", async () => { + const { resolvePinnedUsage } = await import("../../../packages/app/src/usage/pinned.js"); + const preferences = { + displayAs: "used" as const, + serverId: null, + pins: [{ sourceId: "codex", windowId: "weekly" }], + }; + for (const primaryWeekly of [false, true]) { + const weekly = { used_percent: 11, limit_window_seconds: 604800 }; + const report = await fetchUsage(authInput(fixtureHome), async () => + Response.json({ + rate_limit: { + primary_window: primaryWeekly ? weekly : { used_percent: 4, limit_window_seconds: 18000 }, + secondary_window: primaryWeekly ? null : weekly, + }, + }), + ); + const entry = { + id: "codex:account", + sourceId: "codex", + sourceLabel: "Codex", + account: { key: "account" }, + fetchedAt: "2026-10-02T00:00:00Z", + report, + }; + expect(resolvePinnedUsage([entry], preferences)).toMatchObject([ + { windows: [{ key: "codex:account/weekly", percentText: "11%", shortLabel: "wk" }] }, + ]); + // The old slot ID is ambiguous: do not silently retarget a saved Session pin. + expect( + resolvePinnedUsage([entry], { + ...preferences, + pins: [{ sourceId: "codex", windowId: "session" }], + }), + ).toEqual([]); + } +}); + +test("unknown lengths and code review never get a duration from their slot", async () => { + const report = await fetchUsage(authInput(fixtureHome), async () => + Response.json({ + rate_limit: { + primary_window: { used_percent: 11 }, + secondary_window: { used_percent: 22, limit_window_seconds: null }, + }, + code_review_rate_limit: { + primary_window: { used_percent: 3, limit_window_seconds: 604800 }, + secondary_window: { used_percent: 4, limit_window_seconds: 18000 }, + }, + }), + ); + expect(report).toMatchObject({ + windows: [ + { id: "unknown_primary", label: "Primary limit", shortLabel: "" }, + { id: "unknown_secondary", label: "Secondary limit", shortLabel: "" }, + { id: "code_review:weekly", label: "Code review · Weekly", shortLabel: "Code review wk" }, + { id: "code_review:five_hour", label: "Code review · 5-hour", shortLabel: "Code review 5h" }, + ], + }); +}); diff --git a/plugins/codex-usage-source/server/usage.ts b/plugins/codex-usage-source/server/usage.ts index 106a77336d..7d2d89402f 100644 --- a/plugins/codex-usage-source/server/usage.ts +++ b/plugins/codex-usage-source/server/usage.ts @@ -7,7 +7,7 @@ import { unavailable, type UsageAccount, toneFromUsedPct, - windowFromUsedPct, + windowFromReportedDuration, type UsageReport, type UsageWindow, } from "@getpaseo/plugin/server/usage"; @@ -24,14 +24,29 @@ const authSchema = z.object({ .optional(), }); const number = z.coerce.number().finite(); -const windowSchema = z.object({ used_percent: number.optional(), reset_at: number.optional() }); +const windowSchema = z.object({ + used_percent: number.optional(), + reset_at: number.optional(), + limit_window_seconds: number.nullish(), +}); +const rateLimitSchema = z.object({ + primary_window: windowSchema.nullish(), + secondary_window: windowSchema.nullish(), +}); const responseSchema = z.object({ plan_type: z.string().optional(), email: z.string().optional(), - rate_limit: z - .object({ primary_window: windowSchema.nullish(), secondary_window: windowSchema.nullish() }) + rate_limit: rateLimitSchema.nullish(), + additional_rate_limits: z + .array( + z.object({ + limit_name: z.string().optional(), + metered_feature: z.string().optional(), + rate_limit: rateLimitSchema.nullish(), + }), + ) .nullish(), - code_review_rate_limit: z.object({ primary_window: windowSchema.nullish() }).nullish(), + code_review_rate_limit: rateLimitSchema.nullish(), credits: z.object({ balance: number.optional() }).nullish(), }); @@ -90,17 +105,37 @@ export async function readAuth( }; } -function usageWindow( - spec: { id: string; label: string; shortLabel: string; summary?: boolean }, - value: z.infer | null | undefined, -): UsageWindow | null { - if (!value) return null; - const usedPct = value.used_percent ?? 0; - return windowFromUsedPct({ - ...spec, - utilizationPct: usedPct, - resetsAt: value.reset_at != null ? new Date(value.reset_at * 1000).toISOString() : null, - tone: toneFromUsedPct(usedPct), +function usageWindows({ + rateLimit, + scope, + summary = false, +}: { + rateLimit: z.infer | null | undefined; + scope?: { id: string; label: string }; + summary?: boolean; +}): UsageWindow[] { + if (!rateLimit) return []; + return (["primary_window", "secondary_window"] as const).flatMap((slot) => { + const value = rateLimit[slot]; + if (!value) return []; + const primary = slot === "primary_window"; + // Codex does not always report a length. A slot is then only an unknown limit, + // never evidence of a five-hour or weekly period. + return [ + windowFromReportedDuration({ + durationSeconds: value.limit_window_seconds ?? null, + unknown: { + id: primary ? "unknown_primary" : "unknown_secondary", + label: primary ? "Primary limit" : "Secondary limit", + shortLabel: "", + }, + scope, + summary, + utilizationPct: value.used_percent, + resetsAt: value.reset_at != null ? new Date(value.reset_at * 1000).toISOString() : null, + tone: toneFromUsedPct(value.used_percent), + }), + ]; }); } @@ -135,19 +170,23 @@ export async function fetchUsage( if (text.trim().startsWith("<")) throw new Error("Codex usage API returned HTML"); const usage = responseSchema.parse(JSON.parse(text)); const windows = [ - usageWindow( - { id: "session", label: "Session", shortLabel: "5h", summary: true }, - usage.rate_limit?.primary_window, - ), - usageWindow( - { id: "weekly", label: "Weekly", shortLabel: "wk", summary: true }, - usage.rate_limit?.secondary_window, - ), - usageWindow( - { id: "code_review", label: "Code review", shortLabel: "review" }, - usage.code_review_rate_limit?.primary_window, - ), - ].filter((window): window is UsageWindow => window !== null); + ...usageWindows({ rateLimit: usage.rate_limit, summary: true }), + ...(usage.additional_rate_limits ?? []).flatMap((limit) => { + const identity = limit.metered_feature || limit.limit_name; + if (!identity) return []; // An unnamed limit has no stable quota identity. + return usageWindows({ + rateLimit: limit.rate_limit, + scope: { + id: `limit:${encodeURIComponent(identity)}`, + label: limit.limit_name || identity, + }, + }); + }), + ...usageWindows({ + rateLimit: usage.code_review_rate_limit, + scope: { id: "code_review", label: "Code review" }, + }), + ]; const balance = usage.credits?.balance; return { status: "available", diff --git a/plugins/grok-usage-source/server/usage.test.ts b/plugins/grok-usage-source/server/usage.test.ts index 0d95b0b229..8086b48176 100644 --- a/plugins/grok-usage-source/server/usage.test.ts +++ b/plugins/grok-usage-source/server/usage.test.ts @@ -326,3 +326,46 @@ it.each([401, 403])("reports an existing login rejected with HTTP %i", async (st else process.env["GROK_TOKEN"] = previous; } }); + +it.each([undefined, "USAGE_PERIOD_TYPE_DAILY", "USAGE_PERIOD_TYPE_UNKNOWN_WEEKLY"])( + "does not invent a monthly period for %s", + async (type) => { + const directory = mkdtempSync(join(tmpdir(), "grok-period-")); + try { + const locator = join(directory, "auth.json"); + writeFileSync(locator, JSON.stringify({ access_token: "fixture" })); + const report = await fetchUsage({ store: "file", locator }, async () => + Response.json({ config: { creditUsagePercent: 11, currentPeriod: { type } } }), + ); + expect(report).toMatchObject({ + status: "available", + windows: [ + { + id: type ? `period:${type}` : "period:unknown", + label: type ?? "Current period", + shortLabel: "", + }, + ], + }); + } finally { + rmSync(directory, { recursive: true, force: true }); + } + }, +); + +it("does not call credits monthly when no monthly limit was reported", async () => { + const directory = mkdtempSync(join(tmpdir(), "grok-credits-")); + try { + const locator = join(directory, "auth.json"); + writeFileSync(locator, JSON.stringify({ access_token: "fixture" })); + const report = await fetchUsage({ store: "file", locator }, async () => + Response.json({ config: { used: { val: 11 } } }), + ); + expect(report).toMatchObject({ + status: "available", + balances: [{ id: "credits", label: "Credits", used: 11 }], + }); + } finally { + rmSync(directory, { recursive: true, force: true }); + } +}); diff --git a/plugins/grok-usage-source/server/usage.ts b/plugins/grok-usage-source/server/usage.ts index 5f73aac6b9..5a57ca7ddd 100644 --- a/plugins/grok-usage-source/server/usage.ts +++ b/plugins/grok-usage-source/server/usage.ts @@ -74,15 +74,14 @@ export function extractGrokTokenFromAuth(auth: unknown): string | null { return null; } -function grokMonthlyCreditBalance( - response: z.infer, -): UsageBalance | null { +function grokCreditBalance(response: z.infer): UsageBalance | null { const limit = response.config?.monthlyLimit?.val ?? null; const used = response.config?.used?.val ?? response.usage?.creditUsage ?? null; if (limit === null && used === null) return null; return { - id: "monthly_credits", - label: "Monthly credits", + // Only the named monthly allowance establishes a monthly credit bucket. + id: limit === null ? "credits" : "monthly_credits", + label: limit === null ? "Credits" : "Monthly credits", used, remaining: limit !== null && used !== null ? Math.max(0, limit - used) : null, limit, @@ -95,11 +94,18 @@ function grokUsageWindow(response: z.infer): Usa const percent = response.config?.creditUsagePercent; if (typeof percent !== "number") return null; const period = response.config?.currentPeriod; - const weekly = (period?.type ?? "").toUpperCase().includes("WEEKLY"); + const names = { + USAGE_PERIOD_TYPE_WEEKLY: { id: "weekly", label: "Weekly", shortLabel: "wk" }, + USAGE_PERIOD_TYPE_MONTHLY: { id: "monthly", label: "Monthly", shortLabel: "mo" }, + }; + // An absent or unfamiliar enum does not establish a monthly duration. + const name = names[period?.type as keyof typeof names] ?? { + id: `period:${period?.type || "unknown"}`, + label: period?.type || "Current period", + shortLabel: "", + }; return windowFromUsedPct({ - id: weekly ? "weekly" : "monthly", - label: weekly ? "Weekly" : "Monthly", - shortLabel: weekly ? "wk" : "mo", + ...name, utilizationPct: percent, resetsAt: period?.end ?? null, tone: toneFromUsedPct(percent), @@ -129,7 +135,7 @@ export async function fetchUsage( if (!res.ok) throw new Error(`Grok usage API returned ${res.status}`); const resp = GrokUsageResponseSchema.parse(await res.json()); - const balance = grokMonthlyCreditBalance(resp); + const balance = grokCreditBalance(resp); const window = grokUsageWindow(resp); return { diff --git a/plugins/kimi-usage-source/server/usage.test.ts b/plugins/kimi-usage-source/server/usage.test.ts index c5ba74d004..a67e416272 100644 --- a/plugins/kimi-usage-source/server/usage.test.ts +++ b/plugins/kimi-usage-source/server/usage.test.ts @@ -279,7 +279,7 @@ describe("Kimi usage source usage windows", () => { windows: [ { id: "coding_usage", - label: "Weekly limit", + label: "Usage limit", usedPct: 61, remainingPct: 39, resetsAt: "2026-08-05T00:01:45Z", @@ -428,3 +428,41 @@ it.each([401, 403])("reports an existing login rejected with HTTP %i", async (st else process.env["KIMI_TOKEN"] = previous; } }); + +it("does not invent a weekly duration for unnamed top-level usage", async () => { + const directory = mkdtempSync(join(tmpdir(), "kimi-period-")); + try { + const locator = join(directory, "auth.json"); + writeFileSync(locator, JSON.stringify({ access_token: "fixture" })); + const report = await fetchUsage({ store: "file", locator }, async () => + Response.json({ usage: { limit: "100", remaining: "74" } }), + ); + expect(report).toMatchObject({ + status: "available", + windows: [{ id: "coding_usage", label: "Usage limit" }], + }); + } finally { + rmSync(directory, { recursive: true, force: true }); + } +}); + +it("does not infer a duration from an unfamiliar time-unit name containing HOUR", async () => { + const directory = mkdtempSync(join(tmpdir(), "kimi-unit-")); + try { + const locator = join(directory, "auth.json"); + writeFileSync(locator, JSON.stringify({ access_token: "fixture" })); + const report = await fetchUsage({ store: "file", locator }, async () => + Response.json({ + limits: [ + { + window: { duration: 2, timeUnit: "TIME_UNIT_NOT_HOUR" }, + detail: { limit: "100", used: "11" }, + }, + ], + }), + ); + expect(report).toMatchObject({ status: "available", windows: [{ label: "Limit 1" }] }); + } finally { + rmSync(directory, { recursive: true, force: true }); + } +}); diff --git a/plugins/kimi-usage-source/server/usage.ts b/plugins/kimi-usage-source/server/usage.ts index 095b187226..556008990f 100644 --- a/plugins/kimi-usage-source/server/usage.ts +++ b/plugins/kimi-usage-source/server/usage.ts @@ -90,18 +90,18 @@ function durationFrom(value: unknown): number | null { return Number.isFinite(duration) && duration > 0 ? duration : null; } -function durationLabel(duration: number, timeUnit: string | undefined): string | null { - if (!timeUnit) return null; +function durationLabel(duration: number | null, timeUnit: string | undefined): string | null { + if (duration === null || !timeUnit) return null; const normalizedUnit = timeUnit.replace(/^TIME_UNIT_/i, "").toUpperCase(); - if (normalizedUnit.includes("MINUTE")) { + if (normalizedUnit === "MINUTE") { if (duration % 60 === 0) return `${duration / 60}-hour limit`; return `${duration}-minute limit`; } - if (normalizedUnit.includes("HOUR")) return `${duration}-hour limit`; - if (normalizedUnit.includes("DAY")) return `${duration}-day limit`; - if (normalizedUnit.includes("WEEK")) return `${duration}-week limit`; - if (normalizedUnit.includes("SECOND")) return `${duration}-second limit`; + if (normalizedUnit === "HOUR") return `${duration}-hour limit`; + if (normalizedUnit === "DAY") return `${duration}-day limit`; + if (normalizedUnit === "WEEK") return `${duration}-week limit`; + if (normalizedUnit === "SECOND") return `${duration}-second limit`; return null; } @@ -201,7 +201,11 @@ function kimiUsageWindowsFromPayload(payload: unknown): UsageWindow[] { windows.push( windowFromFields({ id: uniqueWindowId("coding_usage", seenWindowIds), - label: explicitUsageLabel(usage) ?? "Weekly limit", + // Top-level usage reports no period unless it supplies duration metadata. + label: + explicitUsageLabel(usage) ?? + durationLabel(durationFrom(usage.duration), usage.timeUnit) ?? + "Usage limit", fields: usage, }), ); diff --git a/plugins/muse-provider/paseo-plugin.json b/plugins/muse-provider/paseo-plugin.json index a53be3fef5..6b632ab2bc 100644 --- a/plugins/muse-provider/paseo-plugin.json +++ b/plugins/muse-provider/paseo-plugin.json @@ -1 +1 @@ -{ "id": "muse-provider", "requirements": { "paseo": ">=0.10.0" } } +{ "id": "muse-provider", "requirements": { "paseo": ">=0.11.0" } } diff --git a/plugins/muse-provider/server/usage.ts b/plugins/muse-provider/server/usage.ts index 85cfb473b6..a8c77428d6 100644 --- a/plugins/muse-provider/server/usage.ts +++ b/plugins/muse-provider/server/usage.ts @@ -7,6 +7,7 @@ import { hashAccountKey, unavailable, windowFromUsedPct, + windowFromReportedDuration, toneFromUsedPct, } from "@getpaseo/plugin/server/usage"; @@ -76,10 +77,9 @@ export class Usage { status: "available", planLabel: usage.tier, windows: [ - windowFromUsedPct({ - id: "five_hour", - label: `${usage.window.windowDurationMins / 60} hours`, - shortLabel: `${usage.window.windowDurationMins / 60}h`, + windowFromReportedDuration({ + durationSeconds: usage.window.windowDurationMins * 60, + unknown: { id: "window", label: "Current window", shortLabel: "" }, summary: true, utilizationPct: usage.window.usedPercent, resetsAt: new Date(usage.window.resetsAtMs).toISOString(), diff --git a/plugins/muse-provider/test/provider.test.ts b/plugins/muse-provider/test/provider.test.ts index 4393cda7ee..ffb7c0468e 100644 --- a/plugins/muse-provider/test/provider.test.ts +++ b/plugins/muse-provider/test/provider.test.ts @@ -1171,7 +1171,7 @@ for (const usage of [ windows: [ { id: "five_hour", - label: "5 hours", + label: "5-hour", usedPct: 75, remainingPct: 25, tone: "warning", @@ -1318,3 +1318,25 @@ test.each(["no sessions", "unrelated environment"])("Muse discovery is empty wit const { Usage } = await import("../server/usage.js"); expect(await new Usage().registration().discover()).toEqual([]); }); + +test("Muse window identity follows the reported duration instead of assuming five hours", async () => { + const h = await harness("catalog-controls", { + MUSE_TEST_USAGE: JSON.stringify({ + usage: { + observedAtMs: 1700000000000, + tier: "Pro", + window: { usedPercent: 11, resetsAtMs: 1700018000000, windowDurationMins: 120 }, + weekly: { usedPercent: 22, resetsAtMs: 1700604800000 }, + }, + }), + }); + await h.open(); + const [account] = await h.usageSource.discover(); + expect(await h.usageSource.fetch(account!.input)).toMatchObject({ + status: "available", + windows: [ + { id: "7200s", label: "2-hour", shortLabel: "2h" }, + { id: "weekly", label: "Weekly", shortLabel: "wk" }, + ], + }); +}); diff --git a/public-docs/plugins/reference.md b/public-docs/plugins/reference.md index f92a6b06a4..188e88c154 100644 --- a/public-docs/plugins/reference.md +++ b/public-docs/plugins/reference.md @@ -363,6 +363,27 @@ inputs tried in order until a report is `available`. An unavailable report, erro fetch falls through to the next input. If none succeeds, the card carries the last report. Discovery failures are logged by the daemon and produce no card. +Window names must come from provider data: an explicit duration, a named API field such as +`five_hour` or `weekly`, or the provider's own period name. Response slots and reset countdowns +do not establish duration. When the provider omits it, use a neutral name such as “Rolling” or +“Primary limit” and explain the missing metadata next to the adapter. + +For numeric durations, use `windowFromReportedDuration()` from +`@getpaseo/plugin/server/usage` (Paseo 0.11+). Pass the reported seconds or `null`, a neutral +`unknown` identity and name, and an optional stable quota `scope`. The helper derives the ID, +label, and short label together; it accepts no duration-label override. For named API periods, +use `windowFromUsedPct()` with names justified by that field. + +A window ID identifies a quota scope and period, never its response position, utilization, or +reset instant. Scope model-specific quotas by the provider's stable feature ID, falling back to +the reported limit name when no ID exists. Preserve IDs across slot moves and display-name +changes. Do not alias an old ambiguous ID to a different period: saved pins match source and +window IDs across all accounts, so users must select the corrected window again. + +`summary: true` selects source defaults until the user customizes pins. The app computes one +effective selection for cards, the sidebar, and toggles. The first edit snapshots those defaults; +an explicit empty selection stays empty. + Re-read the selected store in `fetch()` so the CLI's token rotations take effect. Never redeem refresh tokens or write credential stores: refreshing elsewhere can invalidate the CLI's copy, and rewriting parsed files can discard fields you do not model. If the store disappeared after From 1d0df5c0b737ea1ee9bf774a15c5ad5812eb2f05 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Fri, 2 Oct 2026 14:24:29 +0200 Subject: [PATCH 058/100] docs(changelog): add 0.10.3 release notes --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0534c54505..186dad677e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -107,6 +107,12 @@ - Fixed iPad chat text keeping its narrow width after the Explorer closes ([#5716](https://github.com/getpaseo/paseo/pull/5716)) - Fixed bundling `@getpaseo/client` from npm failing with `Could not resolve "@getpaseo/relay/e2ee"` ([#5815](https://github.com/getpaseo/paseo/pull/5815)) +## 0.10.3 - 2026-10-02 + +### Added + +- Added a confirmation before a pairing link connects to a new host or to a saved host whose key or relay changed ([#5753](https://github.com/getpaseo/paseo/pull/5753)) + ## 0.10.2 - 2026-09-30 ### Fixed From 6a794b6572ee47434db2254c62e4db57b80c94ac Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Fri, 2 Oct 2026 15:35:45 +0100 Subject: [PATCH 059/100] Show a thanks page with community and sponsor links after downloading (#5914) * feat(website): add a thanks page after downloading Desktop and APK download buttons on /download and the homepage now open /download/thanks, which starts the download and points people to Discord, GitHub, Reddit, and sponsoring while it runs. The button carries the asset name rather than the file URL, so the page resolves the URL from the release data itself and cannot be pointed at an arbitrary link. * fix(website): start the exact file the visitor clicked on the thanks page The thanks page looked the file up again from an asset key and channel, so it could start a different release than the button showed (a release published in between, or a beta link without a beta). Download links now carry the release file URL itself; the thanks page accepts only URLs under this repo's GitHub releases, after URL parsing normalizes `..` segments. The e2e test waits for the file request instead of a download event, because WebKit reports no download event for a stubbed response. * test(website): assert the thanks-page download event outside WebKit * test(website): split the thanks-page download check out of the shared test --- packages/website/e2e/download-thanks.spec.ts | 47 +++++++++ .../website/src/components/brand-icons.tsx | 14 +++ .../website/src/components/landing-page.tsx | 2 +- packages/website/src/downloads.test.ts | 36 +++++++ packages/website/src/downloads.tsx | 27 +++++- packages/website/src/routeTree.gen.ts | 21 ++++ packages/website/src/routes/download.tsx | 22 +++-- .../website/src/routes/download_.thanks.tsx | 95 +++++++++++++++++++ 8 files changed, 250 insertions(+), 14 deletions(-) create mode 100644 packages/website/e2e/download-thanks.spec.ts create mode 100644 packages/website/src/downloads.test.ts create mode 100644 packages/website/src/routes/download_.thanks.tsx diff --git a/packages/website/e2e/download-thanks.spec.ts b/packages/website/e2e/download-thanks.spec.ts new file mode 100644 index 0000000000..bc33f8f087 --- /dev/null +++ b/packages/website/e2e/download-thanks.spec.ts @@ -0,0 +1,47 @@ +import { expect, test, type Page } from "playwright/test"; + +const APPLE_SILICON_DMG = /\/getpaseo\/paseo\/releases\/download\/v[^/]+\/Paseo-[^/]+-arm64\.dmg$/; + +async function openDownloadPage(page: Page) { + // Serve a stand-in file so the test never downloads a real release. + await page.route(APPLE_SILICON_DMG, (route) => + route.fulfill({ + status: 200, + headers: { + "content-type": "application/octet-stream", + "content-disposition": "attachment; filename=Paseo.dmg", + }, + body: "dmg", + }), + ); + await page.goto("/download"); +} + +async function clickAppleSilicon(page: Page) { + await page.getByRole("link", { name: "Apple Silicon", exact: true }).click(); +} + +test("a download button opens the thanks page and requests the file", async ({ page }) => { + await openDownloadPage(page); + + const fileRequest = page.waitForRequest(APPLE_SILICON_DMG); + await clickAppleSilicon(page); + + await fileRequest; + await expect(page).toHaveURL(/\/download\/thanks\?file=/); + await expect(page.getByRole("heading", { name: "Thanks for downloading Paseo" })).toBeVisible(); + await expect(page.getByRole("link", { name: "Try again" })).toHaveAttribute( + "href", + APPLE_SILICON_DMG, + ); +}); + +test("the thanks page starts a browser download", async ({ page, browserName }) => { + test.skip(browserName === "webkit", "WebKit reports no download event for a stubbed response"); + await openDownloadPage(page); + + const download = page.waitForEvent("download"); + await clickAppleSilicon(page); + + expect((await download).url()).toMatch(APPLE_SILICON_DMG); +}); diff --git a/packages/website/src/components/brand-icons.tsx b/packages/website/src/components/brand-icons.tsx index c7692b43d6..4e964be8e5 100644 --- a/packages/website/src/components/brand-icons.tsx +++ b/packages/website/src/components/brand-icons.tsx @@ -66,3 +66,17 @@ export function BuyMeACoffeeIcon(props: React.SVGProps) { ); } + +export function RedditIcon(props: React.SVGProps) { + return ( + + ); +} diff --git a/packages/website/src/components/landing-page.tsx b/packages/website/src/components/landing-page.tsx index 6a29c62235..ab70cd5a22 100644 --- a/packages/website/src/components/landing-page.tsx +++ b/packages/website/src/components/landing-page.tsx @@ -994,7 +994,7 @@ function DesktopDownloadButton({ platform }: { platform: DesktopPlatform }) { const download = getDesktopDownload(useRelease(), platform); const Icon = download.icon; return ( - + Download for {download.label} diff --git a/packages/website/src/downloads.test.ts b/packages/website/src/downloads.test.ts new file mode 100644 index 0000000000..3b6130234a --- /dev/null +++ b/packages/website/src/downloads.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, it } from "vitest"; +import { parseReleaseAssetUrl } from "./downloads"; + +describe("parseReleaseAssetUrl", () => { + it("accepts a Paseo release file", () => { + const url = + "https://github.com/getpaseo/paseo/releases/download/v0.10.3/Paseo-0.10.3-arm64.dmg"; + expect(parseReleaseAssetUrl(url)).toBe(url); + }); + + it("rejects a path that climbs into another repository's releases", () => { + expect( + parseReleaseAssetUrl( + "https://github.com/getpaseo/paseo/releases/download/v1/../../../../evil/repo/releases/download/v1/app.dmg", + ), + ).toBeNull(); + expect( + parseReleaseAssetUrl( + "https://github.com/getpaseo/paseo/releases/download/v1/%2e%2e/%2e%2e/%2e%2e/%2e%2e/evil/repo/app.dmg", + ), + ).toBeNull(); + }); + + it("rejects other hosts and malformed values", () => { + expect( + parseReleaseAssetUrl( + "https://github.com.evil.test/getpaseo/paseo/releases/download/v1/a.dmg", + ), + ).toBeNull(); + expect( + parseReleaseAssetUrl("http://github.com/getpaseo/paseo/releases/download/v1/a.dmg"), + ).toBeNull(); + expect(parseReleaseAssetUrl("not a url")).toBeNull(); + expect(parseReleaseAssetUrl(undefined)).toBeNull(); + }); +}); diff --git a/packages/website/src/downloads.tsx b/packages/website/src/downloads.tsx index cce40a7791..0f55f99acf 100644 --- a/packages/website/src/downloads.tsx +++ b/packages/website/src/downloads.tsx @@ -27,6 +27,27 @@ export function downloadUrls(release: ReleaseAssetInfo) { }; } +const RELEASE_ASSETS_ORIGIN = "https://github.com"; +const RELEASE_ASSETS_PATH = "/getpaseo/paseo/releases/download/"; + +/** + * The release file a thanks-page link asks for, or null when it is not one of + * ours. Parsing normalizes `..` segments, so a link cannot climb out of this + * repo's releases into another repo's. + */ +export function parseReleaseAssetUrl(value: unknown): string | null { + if (typeof value !== "string" || !URL.canParse(value)) return null; + const url = new URL(value); + if (url.origin !== RELEASE_ASSETS_ORIGIN) return null; + if (!url.pathname.startsWith(RELEASE_ASSETS_PATH)) return null; + return url.href; +} + +/** Download buttons link here; the thanks page starts the download itself. */ +export function thanksPageHref(fileUrl: string): string { + return `/download/thanks?${new URLSearchParams({ file: fileUrl })}`; +} + export const appStoreUrl = "https://apps.apple.com/app/paseo-pocket-engineer/id6758887924"; export const playStoreUrl = "https://play.google.com/store/apps/details?id=sh.paseo"; export const webAppUrl = "https://app.paseo.sh"; @@ -44,11 +65,11 @@ export function getDesktopDownload( const urls = downloadUrls(release); switch (platform) { case "windows": - return { label: "Windows", href: urls.windowsExeX64, icon: WindowsIcon }; + return { label: "Windows", href: thanksPageHref(urls.windowsExeX64), icon: WindowsIcon }; case "linux": - return { label: "Linux", href: urls.linuxAppImage, icon: LinuxIcon }; + return { label: "Linux", href: thanksPageHref(urls.linuxAppImage), icon: LinuxIcon }; case "mac": - return { label: "Mac", href: urls.macAppleSilicon, icon: AppleIcon }; + return { label: "Mac", href: thanksPageHref(urls.macAppleSilicon), icon: AppleIcon }; } } diff --git a/packages/website/src/routeTree.gen.ts b/packages/website/src/routeTree.gen.ts index 11d3106617..9b2a842b0d 100644 --- a/packages/website/src/routeTree.gen.ts +++ b/packages/website/src/routeTree.gen.ts @@ -62,6 +62,7 @@ import { Route as AgentsRouteImport } from "./routes/agents"; import { Route as IndexRouteImport } from "./routes/index"; import { Route as DocsIndexRouteImport } from "./routes/docs/index"; import { Route as BlogIndexRouteImport } from "./routes/blog/index"; +import { Route as DownloadThanksRouteImport } from "./routes/download_.thanks"; import { Route as DocsSplatRouteImport } from "./routes/docs/$"; import { Route as BlogSplatRouteImport } from "./routes/blog/$"; import { Route as AlternativesSupersetRouteImport } from "./routes/alternatives/superset"; @@ -338,6 +339,11 @@ const BlogIndexRoute = BlogIndexRouteImport.update({ path: "/", getParentRoute: () => BlogRoute, } as any); +const DownloadThanksRoute = DownloadThanksRouteImport.update({ + id: "/download_/thanks", + path: "/download/thanks", + getParentRoute: () => rootRouteImport, +} as any); const DocsSplatRoute = DocsSplatRouteImport.update({ id: "/$", path: "/$", @@ -453,6 +459,7 @@ export interface FileRoutesByFullPath { "/alternatives/superset": typeof AlternativesSupersetRoute; "/blog/$": typeof BlogSplatRoute; "/docs/$": typeof DocsSplatRoute; + "/download/thanks": typeof DownloadThanksRoute; "/blog/": typeof BlogIndexRoute; "/docs/": typeof DocsIndexRoute; } @@ -516,6 +523,7 @@ export interface FileRoutesByTo { "/alternatives/superset": typeof AlternativesSupersetRoute; "/blog/$": typeof BlogSplatRoute; "/docs/$": typeof DocsSplatRoute; + "/download/thanks": typeof DownloadThanksRoute; "/blog": typeof BlogIndexRoute; "/docs": typeof DocsIndexRoute; } @@ -582,6 +590,7 @@ export interface FileRoutesById { "/alternatives/superset": typeof AlternativesSupersetRoute; "/blog/$": typeof BlogSplatRoute; "/docs/$": typeof DocsSplatRoute; + "/download_/thanks": typeof DownloadThanksRoute; "/blog/": typeof BlogIndexRoute; "/docs/": typeof DocsIndexRoute; } @@ -649,6 +658,7 @@ export interface FileRouteTypes { | "/alternatives/superset" | "/blog/$" | "/docs/$" + | "/download/thanks" | "/blog/" | "/docs/"; fileRoutesByTo: FileRoutesByTo; @@ -712,6 +722,7 @@ export interface FileRouteTypes { | "/alternatives/superset" | "/blog/$" | "/docs/$" + | "/download/thanks" | "/blog" | "/docs"; id: @@ -777,6 +788,7 @@ export interface FileRouteTypes { | "/alternatives/superset" | "/blog/$" | "/docs/$" + | "/download_/thanks" | "/blog/" | "/docs/"; fileRoutesById: FileRoutesById; @@ -841,6 +853,7 @@ export interface RootRouteChildren { AlternativesOpencodeDesktopRoute: typeof AlternativesOpencodeDesktopRoute; AlternativesOrcaRoute: typeof AlternativesOrcaRoute; AlternativesSupersetRoute: typeof AlternativesSupersetRoute; + DownloadThanksRoute: typeof DownloadThanksRoute; } declare module "@tanstack/react-router" { @@ -1216,6 +1229,13 @@ declare module "@tanstack/react-router" { preLoaderRoute: typeof BlogIndexRouteImport; parentRoute: typeof BlogRoute; }; + "/download_/thanks": { + id: "/download_/thanks"; + path: "/download/thanks"; + fullPath: "/download/thanks"; + preLoaderRoute: typeof DownloadThanksRouteImport; + parentRoute: typeof rootRouteImport; + }; "/docs/$": { id: "/docs/$"; path: "/$"; @@ -1373,6 +1393,7 @@ const rootRouteChildren: RootRouteChildren = { AlternativesOpencodeDesktopRoute: AlternativesOpencodeDesktopRoute, AlternativesOrcaRoute: AlternativesOrcaRoute, AlternativesSupersetRoute: AlternativesSupersetRoute, + DownloadThanksRoute: DownloadThanksRoute, }; export const routeTree = rootRouteImport ._addFileChildren(rootRouteChildren) diff --git a/packages/website/src/routes/download.tsx b/packages/website/src/routes/download.tsx index f2623e37a4..b7f7331d2f 100644 --- a/packages/website/src/routes/download.tsx +++ b/packages/website/src/routes/download.tsx @@ -8,6 +8,7 @@ import { downloadUrls, appStoreUrl, playStoreUrl, + thanksPageHref, webAppUrl, AppleIcon, AndroidIcon, @@ -88,8 +89,8 @@ function Download() {
- - + + Requires macOS 13 or newer
@@ -104,18 +105,20 @@ function Download() { - {urls.windowsExeArm64 && } + {urls.windowsExeArm64 && ( + + )} - - - + + + @@ -132,7 +135,7 @@ function Download() { {!onBeta && } - + @@ -304,8 +307,7 @@ function DownloadPill({ return (
{label} diff --git a/packages/website/src/routes/download_.thanks.tsx b/packages/website/src/routes/download_.thanks.tsx new file mode 100644 index 0000000000..8bbd99a121 --- /dev/null +++ b/packages/website/src/routes/download_.thanks.tsx @@ -0,0 +1,95 @@ +import { useEffect } from "react"; +import { createFileRoute } from "@tanstack/react-router"; +import { DiscordIcon, GitHubIcon, RedditIcon } from "~/components/brand-icons"; +import { SiteShell } from "~/components/site-shell"; +import { SponsorSection } from "~/components/sponsorship"; +import { parseReleaseAssetUrl } from "~/downloads"; +import { pageMeta } from "~/meta"; +import "~/styles.css"; + +interface ThanksSearch { + file?: string; +} + +export const Route = createFileRoute("/download_/thanks")({ + validateSearch: (search: Record): ThanksSearch => { + const file = parseReleaseAssetUrl(search.file); + return file ? { file } : {}; + }, + head: () => + pageMeta( + "Thanks for downloading Paseo", + "Your Paseo download is starting. Join the community on Discord, GitHub, and Reddit, or sponsor Paseo.", + "/download/thanks", + ), + component: DownloadThanks, +}); + +const COMMUNITY = [ + { + name: "Discord", + icon: , + body: "Get help and share feedback.", + href: "https://discord.gg/jz8T2uahpH", + }, + { + name: "GitHub", + icon: , + body: "Star, report issues, contribute.", + href: "https://github.com/getpaseo/paseo", + }, + { + name: "Reddit", + icon: , + body: "Workflows and questions.", + href: "https://www.reddit.com/r/PaseoAI/", + }, +]; + +function DownloadThanks() { + const { file } = Route.useSearch(); + + useEffect(() => { + if (file) window.location.assign(file); + }, [file]); + + return ( + +

Thanks for downloading Paseo

+

+ Get help, share feedback, and follow along. +

+ {file && ( +

+ Download didn't start?{" "} + + Try again + +

+ )} + +
+
+ {COMMUNITY.map((item) => ( + + {item.icon} + + + {item.name} + + {item.body} + + + ))} +
+ +
+ + ); +} From ee0b0ef7c612d66ede1b8519ad1025134cf14b49 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Fri, 2 Oct 2026 16:40:10 +0100 Subject: [PATCH 060/100] Feature OMP on the homepage, give /agents icons, and use Inter on the website (#5918) * feat(website): feature OMP, add agent icons to /agents, and use Inter Featured agents on the homepage are now Claude Code, Codex, OpenCode, Pi, OMP, Cursor, Muse Code, and Antigravity, driven by one list so the hero badges, the provider cards, and the "+N more" count stay in sync. The count previously subtracted a stale constant (5) while 7 agents were shown. /agents moves onto the shared SiteShell like Hub and Changelog and lists every agent as an icon and name. Icons for ACP agents are vendored from the app's provider catalog rather than imported across packages. The site switches to self-hosted Inter so typography no longer depends on the visitor's OS; system-ui fell back to DejaVu Sans on many Linux setups. * fix(website): keep vendored agent marks out of the homepage bundle The homepage only shows the eight featured agents, which have inline icon components, but importing AgentPageIcon pulled all 31 vendored SVGs into its bundle. AgentPageIcon now lives in its own module used only by /agents. The Codebuddy mark drew past its 16x16 viewBox and was clipped; the viewBox now matches the artwork's bounds. --- package-lock.json | 10 + packages/website/package.json | 1 + .../src/assets/agent-icons/agoragentic.svg | 5 + .../website/src/assets/agent-icons/amp.svg | 6 + .../website/src/assets/agent-icons/auggie.svg | 4 + .../src/assets/agent-icons/autohand.svg | 26 ++ .../website/src/assets/agent-icons/cline.svg | 6 + .../src/assets/agent-icons/codebuddy.svg | 5 + .../src/assets/agent-icons/copilot.svg | 5 + .../src/assets/agent-icons/cortex-code.svg | 3 + .../website/src/assets/agent-icons/corust.svg | 10 + .../website/src/assets/agent-icons/crow.svg | 9 + .../src/assets/agent-icons/deepagents.svg | 6 + .../src/assets/agent-icons/dimcode.svg | 3 + .../website/src/assets/agent-icons/dirac.svg | 6 + .../src/assets/agent-icons/factory-droid.svg | 1 + .../src/assets/agent-icons/fast-agent.svg | 293 ++++++++++++++++++ .../website/src/assets/agent-icons/gemini.svg | 3 + .../website/src/assets/agent-icons/glm.svg | 1 + .../website/src/assets/agent-icons/goose.svg | 3 + .../website/src/assets/agent-icons/grok.svg | 4 + .../website/src/assets/agent-icons/junie.svg | 5 + .../website/src/assets/agent-icons/kilo.svg | 3 + .../website/src/assets/agent-icons/kimi.svg | 3 + .../src/assets/agent-icons/minion-code.svg | 19 ++ .../src/assets/agent-icons/mistral-vibe.svg | 12 + .../website/src/assets/agent-icons/nova.svg | 10 + .../src/assets/agent-icons/poolside.svg | 3 + .../website/src/assets/agent-icons/qoder.svg | 3 + .../src/assets/agent-icons/qwen-code.svg | 1 + .../website/src/assets/agent-icons/sigit.svg | 98 ++++++ .../src/assets/agent-icons/stakpak.svg | 3 + .../website/src/assets/agent-icons/vtcode.svg | 4 + .../website/src/components/agent-icons.tsx | 16 +- .../src/components/agent-page-icon.tsx | 57 ++++ .../website/src/components/landing-page.tsx | 63 ++-- packages/website/src/data/agent-pages.ts | 56 ++-- packages/website/src/routes/agents.tsx | 87 +++--- packages/website/src/styles.css | 3 + 39 files changed, 745 insertions(+), 111 deletions(-) create mode 100644 packages/website/src/assets/agent-icons/agoragentic.svg create mode 100644 packages/website/src/assets/agent-icons/amp.svg create mode 100644 packages/website/src/assets/agent-icons/auggie.svg create mode 100644 packages/website/src/assets/agent-icons/autohand.svg create mode 100644 packages/website/src/assets/agent-icons/cline.svg create mode 100644 packages/website/src/assets/agent-icons/codebuddy.svg create mode 100644 packages/website/src/assets/agent-icons/copilot.svg create mode 100644 packages/website/src/assets/agent-icons/cortex-code.svg create mode 100644 packages/website/src/assets/agent-icons/corust.svg create mode 100644 packages/website/src/assets/agent-icons/crow.svg create mode 100644 packages/website/src/assets/agent-icons/deepagents.svg create mode 100644 packages/website/src/assets/agent-icons/dimcode.svg create mode 100644 packages/website/src/assets/agent-icons/dirac.svg create mode 100644 packages/website/src/assets/agent-icons/factory-droid.svg create mode 100644 packages/website/src/assets/agent-icons/fast-agent.svg create mode 100644 packages/website/src/assets/agent-icons/gemini.svg create mode 100644 packages/website/src/assets/agent-icons/glm.svg create mode 100644 packages/website/src/assets/agent-icons/goose.svg create mode 100644 packages/website/src/assets/agent-icons/grok.svg create mode 100644 packages/website/src/assets/agent-icons/junie.svg create mode 100644 packages/website/src/assets/agent-icons/kilo.svg create mode 100644 packages/website/src/assets/agent-icons/kimi.svg create mode 100644 packages/website/src/assets/agent-icons/minion-code.svg create mode 100644 packages/website/src/assets/agent-icons/mistral-vibe.svg create mode 100644 packages/website/src/assets/agent-icons/nova.svg create mode 100644 packages/website/src/assets/agent-icons/poolside.svg create mode 100644 packages/website/src/assets/agent-icons/qoder.svg create mode 100644 packages/website/src/assets/agent-icons/qwen-code.svg create mode 100644 packages/website/src/assets/agent-icons/sigit.svg create mode 100644 packages/website/src/assets/agent-icons/stakpak.svg create mode 100644 packages/website/src/assets/agent-icons/vtcode.svg create mode 100644 packages/website/src/components/agent-page-icon.tsx diff --git a/package-lock.json b/package-lock.json index 0e65c23a46..102b534819 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6330,6 +6330,15 @@ "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", "license": "MIT" }, + "node_modules/@fontsource-variable/inter": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@fontsource-variable/inter/-/inter-5.3.0.tgz", + "integrity": "sha512-OupL48va4JNofb97w6NYeF9S7W/kHNKM0Er8Dem5nqi4jeOLrVJDoE8tZEpnMJmtkvNbB1EIPPwHcdkF6b1oUA==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, "node_modules/@getpaseo/app": { "resolved": "packages/app", "link": true @@ -40135,6 +40144,7 @@ "dependencies": { "@cloudflare/vite-plugin": "^1.29.1", "@cloudflare/workers-types": "^4.20260317.1", + "@fontsource-variable/inter": "^5.3.0", "@tanstack/react-router": "^1.170.16", "@tanstack/react-start": "^1.168.26", "framer-motion": "^12.35.2", diff --git a/packages/website/package.json b/packages/website/package.json index dcc25b32fa..63924d5a7a 100644 --- a/packages/website/package.json +++ b/packages/website/package.json @@ -14,6 +14,7 @@ "dependencies": { "@cloudflare/vite-plugin": "^1.29.1", "@cloudflare/workers-types": "^4.20260317.1", + "@fontsource-variable/inter": "^5.3.0", "@tanstack/react-router": "^1.170.16", "@tanstack/react-start": "^1.168.26", "framer-motion": "^12.35.2", diff --git a/packages/website/src/assets/agent-icons/agoragentic.svg b/packages/website/src/assets/agent-icons/agoragentic.svg new file mode 100644 index 0000000000..b1372e6835 --- /dev/null +++ b/packages/website/src/assets/agent-icons/agoragentic.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/website/src/assets/agent-icons/amp.svg b/packages/website/src/assets/agent-icons/amp.svg new file mode 100644 index 0000000000..314881aff8 --- /dev/null +++ b/packages/website/src/assets/agent-icons/amp.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/website/src/assets/agent-icons/auggie.svg b/packages/website/src/assets/agent-icons/auggie.svg new file mode 100644 index 0000000000..215107744a --- /dev/null +++ b/packages/website/src/assets/agent-icons/auggie.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/website/src/assets/agent-icons/autohand.svg b/packages/website/src/assets/agent-icons/autohand.svg new file mode 100644 index 0000000000..f3bc983c4d --- /dev/null +++ b/packages/website/src/assets/agent-icons/autohand.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/website/src/assets/agent-icons/cline.svg b/packages/website/src/assets/agent-icons/cline.svg new file mode 100644 index 0000000000..aeeafbc61e --- /dev/null +++ b/packages/website/src/assets/agent-icons/cline.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/website/src/assets/agent-icons/codebuddy.svg b/packages/website/src/assets/agent-icons/codebuddy.svg new file mode 100644 index 0000000000..d1f4b1d74f --- /dev/null +++ b/packages/website/src/assets/agent-icons/codebuddy.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/website/src/assets/agent-icons/copilot.svg b/packages/website/src/assets/agent-icons/copilot.svg new file mode 100644 index 0000000000..626d33badc --- /dev/null +++ b/packages/website/src/assets/agent-icons/copilot.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/website/src/assets/agent-icons/cortex-code.svg b/packages/website/src/assets/agent-icons/cortex-code.svg new file mode 100644 index 0000000000..28f87a258e --- /dev/null +++ b/packages/website/src/assets/agent-icons/cortex-code.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/website/src/assets/agent-icons/corust.svg b/packages/website/src/assets/agent-icons/corust.svg new file mode 100644 index 0000000000..9f30636cb0 --- /dev/null +++ b/packages/website/src/assets/agent-icons/corust.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/packages/website/src/assets/agent-icons/crow.svg b/packages/website/src/assets/agent-icons/crow.svg new file mode 100644 index 0000000000..1169116cd9 --- /dev/null +++ b/packages/website/src/assets/agent-icons/crow.svg @@ -0,0 +1,9 @@ + + + diff --git a/packages/website/src/assets/agent-icons/deepagents.svg b/packages/website/src/assets/agent-icons/deepagents.svg new file mode 100644 index 0000000000..abd818ec47 --- /dev/null +++ b/packages/website/src/assets/agent-icons/deepagents.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/website/src/assets/agent-icons/dimcode.svg b/packages/website/src/assets/agent-icons/dimcode.svg new file mode 100644 index 0000000000..1fa31ce884 --- /dev/null +++ b/packages/website/src/assets/agent-icons/dimcode.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/website/src/assets/agent-icons/dirac.svg b/packages/website/src/assets/agent-icons/dirac.svg new file mode 100644 index 0000000000..4fbb06ceea --- /dev/null +++ b/packages/website/src/assets/agent-icons/dirac.svg @@ -0,0 +1,6 @@ + + δ + + + + diff --git a/packages/website/src/assets/agent-icons/factory-droid.svg b/packages/website/src/assets/agent-icons/factory-droid.svg new file mode 100644 index 0000000000..5c6fb8d1ff --- /dev/null +++ b/packages/website/src/assets/agent-icons/factory-droid.svg @@ -0,0 +1 @@ + diff --git a/packages/website/src/assets/agent-icons/fast-agent.svg b/packages/website/src/assets/agent-icons/fast-agent.svg new file mode 100644 index 0000000000..a07fab2886 --- /dev/null +++ b/packages/website/src/assets/agent-icons/fast-agent.svg @@ -0,0 +1,293 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/website/src/assets/agent-icons/gemini.svg b/packages/website/src/assets/agent-icons/gemini.svg new file mode 100644 index 0000000000..588d89c52a --- /dev/null +++ b/packages/website/src/assets/agent-icons/gemini.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/website/src/assets/agent-icons/glm.svg b/packages/website/src/assets/agent-icons/glm.svg new file mode 100644 index 0000000000..d552d2a3d0 --- /dev/null +++ b/packages/website/src/assets/agent-icons/glm.svg @@ -0,0 +1 @@ +Z.ai diff --git a/packages/website/src/assets/agent-icons/goose.svg b/packages/website/src/assets/agent-icons/goose.svg new file mode 100644 index 0000000000..c492885426 --- /dev/null +++ b/packages/website/src/assets/agent-icons/goose.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/website/src/assets/agent-icons/grok.svg b/packages/website/src/assets/agent-icons/grok.svg new file mode 100644 index 0000000000..35db840319 --- /dev/null +++ b/packages/website/src/assets/agent-icons/grok.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/website/src/assets/agent-icons/junie.svg b/packages/website/src/assets/agent-icons/junie.svg new file mode 100644 index 0000000000..54ce5ff9a9 --- /dev/null +++ b/packages/website/src/assets/agent-icons/junie.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/website/src/assets/agent-icons/kilo.svg b/packages/website/src/assets/agent-icons/kilo.svg new file mode 100644 index 0000000000..8af6e96f34 --- /dev/null +++ b/packages/website/src/assets/agent-icons/kilo.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/website/src/assets/agent-icons/kimi.svg b/packages/website/src/assets/agent-icons/kimi.svg new file mode 100644 index 0000000000..4f7547cf79 --- /dev/null +++ b/packages/website/src/assets/agent-icons/kimi.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/website/src/assets/agent-icons/minion-code.svg b/packages/website/src/assets/agent-icons/minion-code.svg new file mode 100644 index 0000000000..eb3d8eb31d --- /dev/null +++ b/packages/website/src/assets/agent-icons/minion-code.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/packages/website/src/assets/agent-icons/mistral-vibe.svg b/packages/website/src/assets/agent-icons/mistral-vibe.svg new file mode 100644 index 0000000000..b13631b96d --- /dev/null +++ b/packages/website/src/assets/agent-icons/mistral-vibe.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/packages/website/src/assets/agent-icons/nova.svg b/packages/website/src/assets/agent-icons/nova.svg new file mode 100644 index 0000000000..5e19f58879 --- /dev/null +++ b/packages/website/src/assets/agent-icons/nova.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/packages/website/src/assets/agent-icons/poolside.svg b/packages/website/src/assets/agent-icons/poolside.svg new file mode 100644 index 0000000000..91de4c46d4 --- /dev/null +++ b/packages/website/src/assets/agent-icons/poolside.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/website/src/assets/agent-icons/qoder.svg b/packages/website/src/assets/agent-icons/qoder.svg new file mode 100644 index 0000000000..417d83693d --- /dev/null +++ b/packages/website/src/assets/agent-icons/qoder.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/website/src/assets/agent-icons/qwen-code.svg b/packages/website/src/assets/agent-icons/qwen-code.svg new file mode 100644 index 0000000000..6a33da557a --- /dev/null +++ b/packages/website/src/assets/agent-icons/qwen-code.svg @@ -0,0 +1 @@ + diff --git a/packages/website/src/assets/agent-icons/sigit.svg b/packages/website/src/assets/agent-icons/sigit.svg new file mode 100644 index 0000000000..334fc95cba --- /dev/null +++ b/packages/website/src/assets/agent-icons/sigit.svg @@ -0,0 +1,98 @@ + + + + + + + + + + + + + diff --git a/packages/website/src/assets/agent-icons/stakpak.svg b/packages/website/src/assets/agent-icons/stakpak.svg new file mode 100644 index 0000000000..64425076ed --- /dev/null +++ b/packages/website/src/assets/agent-icons/stakpak.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/website/src/assets/agent-icons/vtcode.svg b/packages/website/src/assets/agent-icons/vtcode.svg new file mode 100644 index 0000000000..5ae616428c --- /dev/null +++ b/packages/website/src/assets/agent-icons/vtcode.svg @@ -0,0 +1,4 @@ + + + VT + diff --git a/packages/website/src/components/agent-icons.tsx b/packages/website/src/components/agent-icons.tsx index 8488050b12..fda21cf176 100644 --- a/packages/website/src/components/agent-icons.tsx +++ b/packages/website/src/components/agent-icons.tsx @@ -1,4 +1,4 @@ -// Agent provider brand marks, shared by the landing page and the Hub page. +// Agent provider brand marks, shared by the landing page, the Hub page, and /agents. import type * as React from "react"; @@ -83,6 +83,20 @@ export function PiIcon(props: React.SVGProps) { ); } +export function OmpIcon(props: React.SVGProps) { + return ( + + ); +} + export function MuseCodeIcon(props: React.SVGProps) { return ( > = { + "claude-code": ClaudeCodeIcon, + codex: CodexIcon, + opencode: OpenCodeIcon, + pi: PiIcon, + omp: OmpIcon, + cursor: CursorIcon, + "muse-code": MuseCodeIcon, + antigravity: AntigravityIcon, +}; + +// Marks vendored from the app's ACP provider catalog, named by agent page slug. +const VENDORED_AGENT_PAGE_SVGS = import.meta.glob("../assets/agent-icons/*.svg", { + eager: true, + query: "?raw", + import: "default", +}) as Record; + +const VENDORED_AGENT_PAGE_MARKUP = new Map( + Object.entries(VENDORED_AGENT_PAGE_SVGS).map(([path, svg]) => [ + path.slice(path.lastIndexOf("/") + 1, -".svg".length), + { __html: svg }, + ]), +); + +/** The mark for an agent page, falling back to a generic bot for agents without one. */ +export function AgentPageIcon({ slug, className }: { slug: string; className?: string }) { + const Icon = AGENT_PAGE_ICONS[slug]; + if (Icon) return ; + const markup = VENDORED_AGENT_PAGE_MARKUP.get(slug); + if (markup) { + return ( +