From 18cedbe5540d5ee77623bbbabdf0eca51a2a7a66 Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Thu, 20 Aug 2026 13:36:53 +0200 Subject: [PATCH 01/43] fix(onboarding): reject placeholder/invalid API keys in config writers (#455) Add isValidApiKey() guard to both writeBatchConfig and writeOnboardingConfig: - Rejects known placeholders ('sk-test') - Rejects keys shorter than 10 characters - Rejects empty strings - Allows empty apiKey for OAuth providers (github-copilot) - Existing merge behavior preserves previously-configured providers (e.g. bedrock) Updates test fixtures to use valid-length keys where the test subject is not key validation itself. --- packages/extension/src/credential_scanner.ts | 24 +++ packages/extension/src/onboarding_panel.ts | 17 +- .../extension/test/credential_scanner.test.ts | 155 +++++++++++++++++- .../extension/test/onboarding_panel.test.ts | 2 +- 4 files changed, 193 insertions(+), 5 deletions(-) diff --git a/packages/extension/src/credential_scanner.ts b/packages/extension/src/credential_scanner.ts index c79d4e53..bb0e323d 100644 --- a/packages/extension/src/credential_scanner.ts +++ b/packages/extension/src/credential_scanner.ts @@ -270,12 +270,33 @@ export function webviewSafeResults(credentials: DetectedCredential[]): SafeCrede }); } +// ─── Key validation (#455) ─────────────────────────────────────────────────── + +/** Known placeholder keys that should never be persisted to config. */ +const PLACEHOLDER_KEYS = new Set(["sk-test"]); + +/** + * Returns true if the API key is valid for writing to config. + * Rejects: empty strings, known placeholders, and keys shorter than 10 chars. + * Empty string is allowed ONLY when the caller explicitly passes it (OAuth + * providers like github-copilot don't use API keys at all — they pass empty + * and the entry is written without options.apiKey). This function is called + * only when a key IS present (non-empty), so empty returns false here. + */ +export function isValidApiKey(key: string): boolean { + if (!key || key.trim() === "") return false; + if (PLACEHOLDER_KEYS.has(key.trim())) return false; + if (key.trim().length < 10) return false; + return true; +} + // ─── Batch config writing (AC7) ────────────────────────────────────────────── /** * Write all detected providers to opencode.json in one pass. * The `activeProvider` becomes the active `model` (using its first model entry). * Uses the same schema as writeOnboardingConfig: provider..options.apiKey, env as string[]. + * Credentials with placeholder or invalid keys are silently skipped (#455). */ export function writeBatchConfig( credentials: DetectedCredential[], @@ -301,6 +322,9 @@ export function writeBatchConfig( }; for (const cred of credentials) { + // Skip credentials with invalid/placeholder keys (#455) + if (!isValidApiKey(cred.key)) continue; + const entry: Record = {}; if (cred.key) { entry.options = { apiKey: cred.key }; diff --git a/packages/extension/src/onboarding_panel.ts b/packages/extension/src/onboarding_panel.ts index 61b8c6ea..65054d5e 100644 --- a/packages/extension/src/onboarding_panel.ts +++ b/packages/extension/src/onboarding_panel.ts @@ -17,6 +17,7 @@ import { defaultScanOptions, webviewSafeResults, writeBatchConfig, + isValidApiKey, type DetectedCredential, } from "./credential_scanner"; @@ -93,7 +94,8 @@ function defaultConfigPath(): string { } /** Write the onboarding config to the opencode config file. - * Creates parent directories if needed. Merges with existing config if present. */ + * Creates parent directories if needed. Merges with existing config if present. + * Rejects placeholder/invalid API keys — the provider entry is not written (#455). */ export function writeOnboardingConfig( config: OnboardingConfig, configPath: string = defaultConfigPath(), @@ -110,6 +112,19 @@ export function writeOnboardingConfig( // If parsing fails, start fresh } + // Reject placeholder/invalid keys (#455) — but allow empty keys (OAuth providers) + if (config.apiKey && !isValidApiKey(config.apiKey)) { + // Key is non-empty but invalid — don't write this provider, just preserve existing config + const result = { + ...existing, + $schema: "https://opencode.ai/config.json", + provider: existing.provider ?? {}, + model: config.model, + }; + fs.writeFileSync(configPath, JSON.stringify(result, null, 2) + "\n"); + return; + } + // Provider-specific key env var name const envVarName = providerKeyEnvVar(config.provider); diff --git a/packages/extension/test/credential_scanner.test.ts b/packages/extension/test/credential_scanner.test.ts index 66d36a89..d509f868 100644 --- a/packages/extension/test/credential_scanner.test.ts +++ b/packages/extension/test/credential_scanner.test.ts @@ -590,9 +590,9 @@ describe("scanCredentials — batch config writing integration (AC7)", () => { // Simulate the panel filtering: only passed providers get written const allCredentials: DetectedCredential[] = [ - { provider: "anthropic", key: "sk-ant-pass", source: "env" }, - { provider: "openai", key: "sk-openai-fail", source: "env" }, - { provider: "google", key: "AIza-pass", source: "env" }, + { provider: "anthropic", key: "sk-ant-pass-valid-key", source: "env" }, + { provider: "openai", key: "sk-openai-fail-valid-key", source: "env" }, + { provider: "google", key: "AIza-pass-valid-key-123", source: "env" }, ]; // Simulate testResults: anthropic=true, openai=false, google=true @@ -612,3 +612,152 @@ describe("scanCredentials — batch config writing integration (AC7)", () => { expect(written.provider.openai).toBeUndefined(); }); }); + +// ─── #455: Only write user-selected providers ──────────────────────────────── + +describe("writeBatchConfig — placeholder key rejection (#455 AC5)", () => { + let tmpDir: string; + + beforeEach(() => { + tmpDir = makeTmpDir(); + }); + afterEach(() => { + fs.rmSync(tmpDir, { recursive: true, force: true }); + }); + + it("rejects 'sk-test' placeholder key — does not write provider", async () => { + const { writeBatchConfig } = await import("../src/credential_scanner"); + const credentials: DetectedCredential[] = [ + { provider: "anthropic", key: "sk-test", source: "env" }, + { provider: "openai", key: "sk-openai-real-key-12345", source: "env" }, + ]; + const configPath = path.join(tmpDir, "opencode.json"); + writeBatchConfig(credentials, "openai", configPath); + + const written = JSON.parse(fs.readFileSync(configPath, "utf8")); + expect(written.provider.anthropic).toBeUndefined(); + expect(written.provider.openai).toBeDefined(); + }); + + it("rejects empty string keys — does not write provider", async () => { + const { writeBatchConfig } = await import("../src/credential_scanner"); + const credentials: DetectedCredential[] = [ + { provider: "anthropic", key: "", source: "env" }, + { provider: "openai", key: "sk-openai-real-key-12345", source: "env" }, + ]; + const configPath = path.join(tmpDir, "opencode.json"); + writeBatchConfig(credentials, "openai", configPath); + + const written = JSON.parse(fs.readFileSync(configPath, "utf8")); + expect(written.provider.anthropic).toBeUndefined(); + expect(written.provider.openai).toBeDefined(); + }); + + it("rejects keys shorter than 10 characters — does not write provider", async () => { + const { writeBatchConfig } = await import("../src/credential_scanner"); + const credentials: DetectedCredential[] = [ + { provider: "anthropic", key: "short", source: "env" }, + { provider: "openai", key: "sk-openai-real-key-12345", source: "env" }, + ]; + const configPath = path.join(tmpDir, "opencode.json"); + writeBatchConfig(credentials, "openai", configPath); + + const written = JSON.parse(fs.readFileSync(configPath, "utf8")); + expect(written.provider.anthropic).toBeUndefined(); + expect(written.provider.openai).toBeDefined(); + }); +}); + +describe("writeOnboardingConfig — placeholder key rejection (#455 AC5)", () => { + let tmpDir: string; + + beforeEach(() => { + tmpDir = makeTmpDir(); + }); + afterEach(() => { + fs.rmSync(tmpDir, { recursive: true, force: true }); + }); + + it("rejects 'sk-test' placeholder key — does not write provider entry", async () => { + const { writeOnboardingConfig } = await import("../src/onboarding_panel"); + const configPath = path.join(tmpDir, "opencode.json"); + writeOnboardingConfig( + { provider: "anthropic", model: "anthropic/claude-sonnet-4-5", apiKey: "sk-test" }, + configPath, + ); + + const written = JSON.parse(fs.readFileSync(configPath, "utf8")); + expect(written.provider?.anthropic).toBeUndefined(); + }); + + it("rejects keys shorter than 10 characters — does not write provider entry", async () => { + const { writeOnboardingConfig } = await import("../src/onboarding_panel"); + const configPath = path.join(tmpDir, "opencode.json"); + writeOnboardingConfig( + { provider: "anthropic", model: "anthropic/claude-sonnet-4-5", apiKey: "tiny" }, + configPath, + ); + + const written = JSON.parse(fs.readFileSync(configPath, "utf8")); + expect(written.provider?.anthropic).toBeUndefined(); + }); + + it("allows valid keys (>= 10 chars, not placeholder)", async () => { + const { writeOnboardingConfig } = await import("../src/onboarding_panel"); + const configPath = path.join(tmpDir, "opencode.json"); + writeOnboardingConfig( + { provider: "anthropic", model: "anthropic/claude-sonnet-4-5", apiKey: "sk-ant-valid-key-123456" }, + configPath, + ); + + const written = JSON.parse(fs.readFileSync(configPath, "utf8")); + expect(written.provider.anthropic).toBeDefined(); + expect(written.provider.anthropic.options.apiKey).toBe("sk-ant-valid-key-123456"); + }); + + it("allows empty key for OAuth providers like github-copilot", async () => { + const { writeOnboardingConfig } = await import("../src/onboarding_panel"); + const configPath = path.join(tmpDir, "opencode.json"); + writeOnboardingConfig( + { provider: "github-copilot", model: "github-copilot/claude-sonnet-4-5", apiKey: "" }, + configPath, + ); + + const written = JSON.parse(fs.readFileSync(configPath, "utf8")); + // OAuth providers write with empty key (no options.apiKey) — that's valid + expect(written.provider["github-copilot"]).toBeDefined(); + }); +}); + +describe("writeBatchConfig — preserves existing providers via merge (#455 AC6)", () => { + let tmpDir: string; + + beforeEach(() => { + tmpDir = makeTmpDir(); + }); + afterEach(() => { + fs.rmSync(tmpDir, { recursive: true, force: true }); + }); + + it("preserves existing amazon-bedrock entry when writing new providers", async () => { + const { writeBatchConfig } = await import("../src/credential_scanner"); + const credentials: DetectedCredential[] = [ + { provider: "openai", key: "sk-openai-real-key-12345", source: "env" }, + ]; + const configPath = path.join(tmpDir, "opencode.json"); + + // Pre-populate with existing bedrock config (as if provisioned via cloud_key flow) + fs.writeFileSync(configPath, JSON.stringify({ + provider: { "amazon-bedrock": { options: { apiKey: "ABSK-service-credential-xyz" } } }, + })); + + writeBatchConfig(credentials, "openai", configPath); + + const written = JSON.parse(fs.readFileSync(configPath, "utf8")); + // Bedrock preserved from existing config via merge + expect(written.provider["amazon-bedrock"]).toBeDefined(); + expect(written.provider["amazon-bedrock"].options.apiKey).toBe("ABSK-service-credential-xyz"); + // User-selected provider also present + expect(written.provider.openai).toBeDefined(); + }); +}); diff --git a/packages/extension/test/onboarding_panel.test.ts b/packages/extension/test/onboarding_panel.test.ts index 39fcad66..404b571c 100644 --- a/packages/extension/test/onboarding_panel.test.ts +++ b/packages/extension/test/onboarding_panel.test.ts @@ -197,7 +197,7 @@ describe("writeOnboardingConfig — config file writing (AC5)", () => { fs.writeFileSync(configPath, JSON.stringify({ permission: { bash: "allow" } })); writeOnboardingConfig( - { provider: "anthropic", model: "anthropic/claude-sonnet-4-5", apiKey: "sk-x" }, + { provider: "anthropic", model: "anthropic/claude-sonnet-4-5", apiKey: "sk-ant-valid-key-123456" }, configPath, ); From 40bb07fe5204801422e8be2c4c474aef581cfd20 Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Thu, 20 Aug 2026 13:38:35 +0200 Subject: [PATCH 02/43] fix(onboarding): default import checkboxes to unchecked (opt-in) (#455) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The auto-import UI now defaults all provider checkboxes to unchecked. Providers are auto-checked only when their connection test passes — giving the user explicit control over which providers enter their config. - Checkboxes start unchecked, radios start disabled - Passing a connection test auto-checks the provider and enables its radio - Failing a test dims the row and keeps it unchecked - Manual checkbox toggle enables/disables the radio correctly - Instruction text updated to reflect the new behavior --- packages/extension/src/onboarding_webview.ts | 26 ++++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/packages/extension/src/onboarding_webview.ts b/packages/extension/src/onboarding_webview.ts index 1807224c..36c50ff0 100644 --- a/packages/extension/src/onboarding_webview.ts +++ b/packages/extension/src/onboarding_webview.ts @@ -717,17 +717,17 @@ function buildForm(): void { importPreview.innerHTML = `

- Choose which providers to import and pick your default: + Select which providers to import (tested credentials will be auto-selected):

${providers .map( (p, i) => `
@@ -757,7 +757,8 @@ function buildForm(): void {

`; - // Wire checkbox ↔ radio sync: unchecking a provider disables its radio + // Wire checkbox ↔ radio sync: unchecking a provider disables its radio; + // checking enables it const allCheckboxes = document.querySelectorAll('input[name="import-include"]'); allCheckboxes.forEach((cb) => { cb.addEventListener("change", () => { @@ -783,6 +784,9 @@ function buildForm(): void { } else { if (row) row.style.opacity = "1"; if (radio) radio.disabled = false; + // If no default is selected, select this one + const anyDefault = document.querySelector('input[name="import-default"]:checked:not(:disabled)') as HTMLInputElement | null; + if (!anyDefault && radio) radio.checked = true; } updateConfirmState(); }); @@ -839,11 +843,23 @@ function buildForm(): void { if (ok) { statusEl.textContent = "✓"; statusEl.style.color = "var(--vscode-testing-iconPassed, #73c991)"; + // Auto-check passing providers and enable their radio (#455: opt-in, but + // passing the test is an explicit signal the credential works) + if (rowEl) { + rowEl.style.opacity = "1"; + const checkbox = rowEl.querySelector('input[name="import-include"]') as HTMLInputElement | null; + const radio = rowEl.querySelector('input[name="import-default"]') as HTMLInputElement | null; + if (checkbox && !checkbox.checked) checkbox.checked = true; + if (radio) radio.disabled = false; + // If no default is selected yet, select this one + const anyDefault = document.querySelector('input[name="import-default"]:checked:not(:disabled)') as HTMLInputElement | null; + if (!anyDefault && radio) radio.checked = true; + } } else { statusEl.textContent = "✗"; statusEl.style.color = "var(--vscode-testing-iconFailed, #f14c4c)"; statusEl.title = error ?? "Connection failed"; - // Uncheck and dim failed providers + // Dim failed providers and ensure they stay unchecked if (rowEl) { rowEl.style.opacity = "0.5"; const checkbox = rowEl.querySelector('input[name="import-include"]') as HTMLInputElement | null; From 1814f64ef03898a6eec934617a9faa3c25bf0749 Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Thu, 20 Aug 2026 17:13:59 +0200 Subject: [PATCH 03/43] fix(onboarding): restart server after config write + replace providers on redo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two fixes so redo-onboarding works correctly: 1. After writing config (both manual and auto-import paths), call amicode.restartServer so the opencode process picks up the new provider settings immediately — no manual reload needed. 2. writeBatchConfig now REPLACES the provider section instead of merging. On redo, the user's explicit selection is the canonical set; stale providers from a previous onboarding don't persist. Non-provider settings (permission, etc.) are still preserved via the top-level merge. --- packages/extension/src/credential_scanner.ts | 7 +++---- packages/extension/src/onboarding_panel.ts | 4 ++++ .../extension/test/credential_scanner.test.ts | 17 ++++++++++------- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/packages/extension/src/credential_scanner.ts b/packages/extension/src/credential_scanner.ts index bb0e323d..0170250b 100644 --- a/packages/extension/src/credential_scanner.ts +++ b/packages/extension/src/credential_scanner.ts @@ -316,10 +316,9 @@ export function writeBatchConfig( // Start fresh if parsing fails } - // Build provider entries - const providerEntry: Record = { - ...(existing.provider as Record ?? {}), - }; + // Build provider entries — replaces the entire provider section + // (on redo, user's selection is the canonical set; old entries don't persist) + const providerEntry: Record = {}; for (const cred of credentials) { // Skip credentials with invalid/placeholder keys (#455) diff --git a/packages/extension/src/onboarding_panel.ts b/packages/extension/src/onboarding_panel.ts index 65054d5e..582cf477 100644 --- a/packages/extension/src/onboarding_panel.ts +++ b/packages/extension/src/onboarding_panel.ts @@ -366,6 +366,8 @@ export function registerOnboardingPanel(ctx: vscode.ExtensionContext): void { writeOnboardingConfig(payload); panel.dispose(); fireOnboardingComplete(); + // Restart server so it picks up the new provider config + void vscode.commands.executeCommand("amicode.restartServer"); // Open chat as fallback (in case no completion listener is wired) void vscode.commands.executeCommand("amicode.openChat"); } else if (msg.type === "cancel") { @@ -446,6 +448,8 @@ export function registerOnboardingPanel(ctx: vscode.ExtensionContext): void { testResults.clear(); panel.dispose(); fireOnboardingComplete(); + // Restart server so it picks up the new provider config + void vscode.commands.executeCommand("amicode.restartServer"); // Open chat as fallback (in case no completion listener is wired) void vscode.commands.executeCommand("amicode.openChat"); } diff --git a/packages/extension/test/credential_scanner.test.ts b/packages/extension/test/credential_scanner.test.ts index d509f868..44103250 100644 --- a/packages/extension/test/credential_scanner.test.ts +++ b/packages/extension/test/credential_scanner.test.ts @@ -729,7 +729,7 @@ describe("writeOnboardingConfig — placeholder key rejection (#455 AC5)", () => }); }); -describe("writeBatchConfig — preserves existing providers via merge (#455 AC6)", () => { +describe("writeBatchConfig — replaces provider section (redo overwrites)", () => { let tmpDir: string; beforeEach(() => { @@ -739,25 +739,28 @@ describe("writeBatchConfig — preserves existing providers via merge (#455 AC6) fs.rmSync(tmpDir, { recursive: true, force: true }); }); - it("preserves existing amazon-bedrock entry when writing new providers", async () => { + it("replaces existing providers with only the selected ones", async () => { const { writeBatchConfig } = await import("../src/credential_scanner"); const credentials: DetectedCredential[] = [ { provider: "openai", key: "sk-openai-real-key-12345", source: "env" }, ]; const configPath = path.join(tmpDir, "opencode.json"); - // Pre-populate with existing bedrock config (as if provisioned via cloud_key flow) + // Pre-populate with existing bedrock config (as if from a previous onboarding) fs.writeFileSync(configPath, JSON.stringify({ provider: { "amazon-bedrock": { options: { apiKey: "ABSK-service-credential-xyz" } } }, + permission: { bash: "allow" }, })); writeBatchConfig(credentials, "openai", configPath); const written = JSON.parse(fs.readFileSync(configPath, "utf8")); - // Bedrock preserved from existing config via merge - expect(written.provider["amazon-bedrock"]).toBeDefined(); - expect(written.provider["amazon-bedrock"].options.apiKey).toBe("ABSK-service-credential-xyz"); - // User-selected provider also present + // Old provider NOT preserved — user didn't select it this time + expect(written.provider["amazon-bedrock"]).toBeUndefined(); + // Only the user-selected provider is present expect(written.provider.openai).toBeDefined(); + expect(written.provider.openai.options.apiKey).toBe("sk-openai-real-key-12345"); + // Non-provider settings are still preserved + expect(written.permission).toEqual({ bash: "allow" }); }); }); From 15ad0eeb1f86e2d99dc5ea9c2bcc49635abf13f3 Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Thu, 20 Aug 2026 17:26:26 +0200 Subject: [PATCH 04/43] feat(onboarding): disconnect excluded providers from opencode auth stores When the user unchecks a provider during onboarding import, its credentials are removed from both account.json (v2) and auth.json (v1). After the server restart, the excluded provider won't auto-connect. - disconnectProviders() handles v2 accounts + active map, and v1 flat entries - 'opencode' exclusion also removes the 'opencode-go' alias - Missing/malformed files are skipped gracefully - 3 TDD tests covering account.json, auth.json, and missing file handling - Wired into confirm-import: excluded = detected - selected --- packages/extension/src/credential_scanner.ts | 64 +++++++++++++++++++ packages/extension/src/onboarding_panel.ts | 8 +++ .../extension/test/credential_scanner.test.ts | 60 +++++++++++++++++ 3 files changed, 132 insertions(+) diff --git a/packages/extension/src/credential_scanner.ts b/packages/extension/src/credential_scanner.ts index 0170250b..c018bc4e 100644 --- a/packages/extension/src/credential_scanner.ts +++ b/packages/extension/src/credential_scanner.ts @@ -348,3 +348,67 @@ export function writeBatchConfig( fs.writeFileSync(targetPath, JSON.stringify(result, null, 2) + "\n"); } + +// ─── Disconnect excluded providers from auth stores ────────────────────────── + +/** + * Remove credentials for excluded providers from opencode's auth stores. + * After a server restart, excluded providers will no longer auto-connect. + */ +export function disconnectProviders( + providers: string[], + options?: { accountJsonPath?: string; authJsonPath?: string }, +): void { + const home = os.homedir(); + const dataDir = path.join(home, ".local", "share", "opencode"); + const accountPath = options?.accountJsonPath ?? path.join(dataDir, "account.json"); + const authPath = options?.authJsonPath ?? path.join(dataDir, "auth.json"); + + // Build the set of serviceIDs to remove, including aliases + const excludeSet = new Set(providers); + if (excludeSet.has("opencode")) excludeSet.add("opencode-go"); + + // Remove from account.json (v2) + try { + const raw = fs.readFileSync(accountPath, "utf8"); + const data = JSON.parse(raw); + if (data.version === 2 && typeof data.accounts === "object" && data.accounts !== null) { + let modified = false; + for (const [id, entry] of Object.entries(data.accounts)) { + const acct = entry as { serviceID?: string }; + if (acct.serviceID && excludeSet.has(acct.serviceID)) { + delete data.accounts[id]; + if (data.active && acct.serviceID in data.active) { + delete data.active[acct.serviceID]; + } + modified = true; + } + } + if (modified) { + fs.writeFileSync(accountPath, JSON.stringify(data, null, 2) + "\n"); + } + } + } catch { + // Skip if file doesn't exist or is malformed + } + + // Remove from auth.json (v1) + try { + const raw = fs.readFileSync(authPath, "utf8"); + const data = JSON.parse(raw); + if (typeof data === "object" && data !== null) { + let modified = false; + for (const serviceId of excludeSet) { + if (serviceId in data) { + delete data[serviceId]; + modified = true; + } + } + if (modified) { + fs.writeFileSync(authPath, JSON.stringify(data, null, 2) + "\n"); + } + } + } catch { + // Skip if file doesn't exist or is malformed + } +} diff --git a/packages/extension/src/onboarding_panel.ts b/packages/extension/src/onboarding_panel.ts index 582cf477..e9d386a0 100644 --- a/packages/extension/src/onboarding_panel.ts +++ b/packages/extension/src/onboarding_panel.ts @@ -17,6 +17,7 @@ import { defaultScanOptions, webviewSafeResults, writeBatchConfig, + disconnectProviders, isValidApiKey, type DetectedCredential, } from "./credential_scanner"; @@ -444,6 +445,13 @@ export function registerOnboardingPanel(ctx: vscode.ExtensionContext): void { if (passedCredentials.length > 0) { writeBatchConfig(passedCredentials, payload.activeProvider); } + // Disconnect providers the user explicitly excluded from auth stores + const excluded = heldCredentials + .map((c) => c.provider) + .filter((p) => !included.has(p)); + if (excluded.length > 0) { + disconnectProviders(excluded); + } heldCredentials = []; testResults.clear(); panel.dispose(); diff --git a/packages/extension/test/credential_scanner.test.ts b/packages/extension/test/credential_scanner.test.ts index 44103250..d78b3b59 100644 --- a/packages/extension/test/credential_scanner.test.ts +++ b/packages/extension/test/credential_scanner.test.ts @@ -12,6 +12,7 @@ import * as os from "node:os"; import { scanCredentials, defaultScanOptions, + disconnectProviders, type DetectedCredential, type ScanOptions, type ScanResult, @@ -764,3 +765,62 @@ describe("writeBatchConfig — replaces provider section (redo overwrites)", () expect(written.permission).toEqual({ bash: "allow" }); }); }); + +// ─── disconnectProviders — remove excluded providers from auth stores ──────── + +describe("disconnectProviders — removes credentials from opencode auth stores", () => { + let tmpDir: string; + + beforeEach(() => { + tmpDir = makeTmpDir(); + }); + afterEach(() => { + fs.rmSync(tmpDir, { recursive: true, force: true }); + }); + + it("removes excluded provider from account.json v2", () => { + const accountPath = writeJson(tmpDir, "account.json", { + version: 2, + accounts: { + acc1: { id: "acc1", serviceID: "opencode", credential: { type: "api", key: "sk-oc" } }, + acc2: { id: "acc2", serviceID: "amazon-bedrock", credential: { type: "api", key: "aws-key" } }, + }, + active: { opencode: "acc1", "amazon-bedrock": "acc2" }, + }); + + disconnectProviders(["opencode"], { accountJsonPath: accountPath, authJsonPath: "/nonexistent" }); + + const result = JSON.parse(fs.readFileSync(accountPath, "utf8")); + // opencode removed + expect(result.accounts.acc1).toBeUndefined(); + expect(result.active.opencode).toBeUndefined(); + // amazon-bedrock preserved + expect(result.accounts.acc2).toBeDefined(); + expect(result.active["amazon-bedrock"]).toBe("acc2"); + }); + + it("removes excluded provider from auth.json v1", () => { + const authPath = writeJson(tmpDir, "auth.json", { + "opencode-go": { type: "api", key: "sk-oc" }, + "amazon-bedrock": { type: "api", key: "aws-key" }, + }); + + disconnectProviders(["opencode"], { accountJsonPath: "/nonexistent", authJsonPath: authPath }); + + const result = JSON.parse(fs.readFileSync(authPath, "utf8")); + // opencode-go removed (alias of opencode) + expect(result["opencode-go"]).toBeUndefined(); + // amazon-bedrock preserved + expect(result["amazon-bedrock"]).toBeDefined(); + }); + + it("handles missing files gracefully", () => { + // Should not throw + expect(() => + disconnectProviders(["opencode"], { + accountJsonPath: "/nonexistent/account.json", + authJsonPath: "/nonexistent/auth.json", + }), + ).not.toThrow(); + }); +}); From 369638d333d78aad581d836a5534042640430f23 Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Thu, 20 Aug 2026 17:39:07 +0200 Subject: [PATCH 05/43] fix(onboarding): don't open chat until server restart completes (TDD) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously confirm-import and config-success called both restartServer AND openChat immediately. The chat panel opened before the server was ready, causing 'could not query /config/providers (fetch failed)'. Now only restartServer is called — the existing onReady-gated listener in extension.ts (line 845) handles opening chat once the server responds. Test: 'confirm-import restarts server but does NOT open chat directly' --- packages/extension/src/onboarding_panel.ts | 10 +++--- .../extension/test/credential_scanner.test.ts | 1 - .../extension/test/onboarding_panel.test.ts | 34 +++++++++++++++++++ 3 files changed, 38 insertions(+), 7 deletions(-) diff --git a/packages/extension/src/onboarding_panel.ts b/packages/extension/src/onboarding_panel.ts index e9d386a0..380b25ec 100644 --- a/packages/extension/src/onboarding_panel.ts +++ b/packages/extension/src/onboarding_panel.ts @@ -367,10 +367,9 @@ export function registerOnboardingPanel(ctx: vscode.ExtensionContext): void { writeOnboardingConfig(payload); panel.dispose(); fireOnboardingComplete(); - // Restart server so it picks up the new provider config + // Restart server so it picks up the new provider config. + // Chat opens via the onReady-gated listener in extension.ts. void vscode.commands.executeCommand("amicode.restartServer"); - // Open chat as fallback (in case no completion listener is wired) - void vscode.commands.executeCommand("amicode.openChat"); } else if (msg.type === "cancel") { // User cancelled onboarding — close panel, re-open chat panel.dispose(); @@ -456,10 +455,9 @@ export function registerOnboardingPanel(ctx: vscode.ExtensionContext): void { testResults.clear(); panel.dispose(); fireOnboardingComplete(); - // Restart server so it picks up the new provider config + // Restart server so it picks up the new provider config. + // Chat opens via the onReady-gated listener in extension.ts. void vscode.commands.executeCommand("amicode.restartServer"); - // Open chat as fallback (in case no completion listener is wired) - void vscode.commands.executeCommand("amicode.openChat"); } }, null, diff --git a/packages/extension/test/credential_scanner.test.ts b/packages/extension/test/credential_scanner.test.ts index d78b3b59..0a391044 100644 --- a/packages/extension/test/credential_scanner.test.ts +++ b/packages/extension/test/credential_scanner.test.ts @@ -468,7 +468,6 @@ describe("scanCredentials — end-to-end with real default paths", () => { it("finds credentials from this machine's actual opencode install", async () => { const result = await scanCredentials(defaultScanOptions()); - // This machine has opencode configured — scan should find at least one provider console.log(` [e2e] Found ${result.credentials.length} credential(s):`); for (const c of result.credentials) { console.log(` ${c.provider} (from ${c.source}) — key ${c.key.slice(0, 6)}...`); diff --git a/packages/extension/test/onboarding_panel.test.ts b/packages/extension/test/onboarding_panel.test.ts index 404b571c..989f81d7 100644 --- a/packages/extension/test/onboarding_panel.test.ts +++ b/packages/extension/test/onboarding_panel.test.ts @@ -494,6 +494,40 @@ describe("Credential import — panel message handling (AC2, AC8, AC12, AC14)", spy.mockRestore(); }); + + it("confirm-import restarts server but does NOT open chat directly (waits for ready)", async () => { + // Clear command execution history + (vscode.commands as { executed: string[] }).executed = []; + + const spy = vi.spyOn(vscode.window, "createWebviewPanel"); + await vscode.commands.executeCommand("amicode.onboarding.open"); + const panel = spy.mock.results[0].value as { + webview: { + postMessage: ReturnType; + _simulateMessage: (msg: unknown) => void; + }; + }; + + const postSpy = vi.fn().mockResolvedValue(true); + panel.webview.postMessage = postSpy; + + // Trigger scan then confirm + panel.webview._simulateMessage({ type: "scan-credentials" }); + await new Promise((r) => setTimeout(r, 50)); + panel.webview._simulateMessage({ + type: "confirm-import", + payload: { activeProvider: "anthropic", includedProviders: ["anthropic"] }, + }); + await new Promise((r) => setTimeout(r, 50)); + + const executed = (vscode.commands as { executed: string[] }).executed; + // Should restart the server + expect(executed).toContain("amicode.restartServer"); + // Should NOT open chat directly (that causes the fetch-failed error) + expect(executed).not.toContain("amicode.openChat"); + + spy.mockRestore(); + }); }); describe("Webview HTML generation (AC2, AC9)", () => { From a48bd3c204d85220cd2456ff9d99cb4631975208 Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Thu, 20 Aug 2026 17:42:39 +0200 Subject: [PATCH 06/43] fix(onboarding): do NOT delete credentials from auth stores on uncheck MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit disconnectProviders was wiping account.json entries when a user unchecked a provider during import. This is wrong — the auth store is a separate concern from the model config. Unchecking means 'don't write this to opencode.json as a model provider', not 'delete my credentials entirely'. The auth store should only be modified through the connections disconnect flow in the settings dialog. Removed the call from confirm-import; the function remains available if needed elsewhere. --- packages/extension/src/onboarding_panel.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/packages/extension/src/onboarding_panel.ts b/packages/extension/src/onboarding_panel.ts index 380b25ec..4f493c2d 100644 --- a/packages/extension/src/onboarding_panel.ts +++ b/packages/extension/src/onboarding_panel.ts @@ -17,7 +17,6 @@ import { defaultScanOptions, webviewSafeResults, writeBatchConfig, - disconnectProviders, isValidApiKey, type DetectedCredential, } from "./credential_scanner"; @@ -444,13 +443,6 @@ export function registerOnboardingPanel(ctx: vscode.ExtensionContext): void { if (passedCredentials.length > 0) { writeBatchConfig(passedCredentials, payload.activeProvider); } - // Disconnect providers the user explicitly excluded from auth stores - const excluded = heldCredentials - .map((c) => c.provider) - .filter((p) => !included.has(p)); - if (excluded.length > 0) { - disconnectProviders(excluded); - } heldCredentials = []; testResults.clear(); panel.dispose(); From 7196edbcb14f75530ae549806b38178c43a01a60 Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Thu, 20 Aug 2026 17:46:42 +0200 Subject: [PATCH 07/43] =?UTF-8?q?test(onboarding):=20safety=20test=20?= =?UTF-8?q?=E2=80=94=20writeBatchConfig=20never=20modifies=20auth=20stores?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Verifies that the onboarding config write (writeBatchConfig) only touches opencode.json and never modifies account.json or auth.json. Auth stores are a separate concern managed by the connections UI. --- .../extension/test/credential_scanner.test.ts | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/packages/extension/test/credential_scanner.test.ts b/packages/extension/test/credential_scanner.test.ts index 0a391044..17c45aaf 100644 --- a/packages/extension/test/credential_scanner.test.ts +++ b/packages/extension/test/credential_scanner.test.ts @@ -763,6 +763,36 @@ describe("writeBatchConfig — replaces provider section (redo overwrites)", () // Non-provider settings are still preserved expect(written.permission).toEqual({ bash: "allow" }); }); + + it("writeBatchConfig never modifies auth stores (account.json / auth.json)", async () => { + const { writeBatchConfig } = await import("../src/credential_scanner"); + const credentials: DetectedCredential[] = [ + { provider: "openai", key: "sk-openai-real-key-12345", source: "env" }, + ]; + const configPath = path.join(tmpDir, "opencode.json"); + + // Set up fake auth stores and record their content + const accountPath = path.join(tmpDir, "account.json"); + const authPath = path.join(tmpDir, "auth.json"); + const accountContent = JSON.stringify({ + version: 2, + accounts: { acc1: { id: "acc1", serviceID: "opencode", credential: { type: "api", key: "sk-oc" } } }, + active: { opencode: "acc1" }, + }); + const authContent = JSON.stringify({ + "opencode-go": { type: "api", key: "sk-old" }, + "amazon-bedrock": { type: "api", key: "aws-key" }, + }); + fs.writeFileSync(accountPath, accountContent); + fs.writeFileSync(authPath, authContent); + + // Write batch config (only touches opencode.json) + writeBatchConfig(credentials, "openai", configPath); + + // Auth stores must be UNTOUCHED + expect(fs.readFileSync(accountPath, "utf8")).toBe(accountContent); + expect(fs.readFileSync(authPath, "utf8")).toBe(authContent); + }); }); // ─── disconnectProviders — remove excluded providers from auth stores ──────── From 191da918a93c3ea734fef4cfa2877199511eb26b Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Thu, 20 Aug 2026 17:54:40 +0200 Subject: [PATCH 08/43] feat(onboarding): disconnect opencode from auth store when unchecked (TDD) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the user unchecks 'opencode' during import, its entries are removed from account.json (opencode + opencode-go alias). This is the only provider that needs file-level removal — it's a built-in integration not reachable via the /connections/disconnect API. Other providers (amazon-bedrock, etc.) are never touched in the auth store — unchecking them just excludes them from opencode.json. Tests: - 'only removes the specified provider — others are preserved' verifies that disconnecting opencode leaves amazon-bedrock intact - Existing safety test confirms writeBatchConfig never touches auth stores - Real auth store checksums verified unchanged after test suite run --- packages/extension/src/onboarding_panel.ts | 7 +++++ .../extension/test/credential_scanner.test.ts | 26 +++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/packages/extension/src/onboarding_panel.ts b/packages/extension/src/onboarding_panel.ts index 4f493c2d..687707e5 100644 --- a/packages/extension/src/onboarding_panel.ts +++ b/packages/extension/src/onboarding_panel.ts @@ -17,6 +17,7 @@ import { defaultScanOptions, webviewSafeResults, writeBatchConfig, + disconnectProviders, isValidApiKey, type DetectedCredential, } from "./credential_scanner"; @@ -443,6 +444,12 @@ export function registerOnboardingPanel(ctx: vscode.ExtensionContext): void { if (passedCredentials.length > 0) { writeBatchConfig(passedCredentials, payload.activeProvider); } + // If user excluded 'opencode', disconnect it from the auth store. + // This is the only provider that needs file-level removal (it's a + // built-in integration, not in the connections seam). + if (!included.has("opencode") && heldCredentials.some((c) => c.provider === "opencode")) { + disconnectProviders(["opencode"]); + } heldCredentials = []; testResults.clear(); panel.dispose(); diff --git a/packages/extension/test/credential_scanner.test.ts b/packages/extension/test/credential_scanner.test.ts index 17c45aaf..eddf9936 100644 --- a/packages/extension/test/credential_scanner.test.ts +++ b/packages/extension/test/credential_scanner.test.ts @@ -852,4 +852,30 @@ describe("disconnectProviders — removes credentials from opencode auth stores" }), ).not.toThrow(); }); + + it("only removes the specified provider — others are preserved", () => { + const accountPath = writeJson(tmpDir, "account.json", { + version: 2, + accounts: { + acc1: { id: "acc1", serviceID: "opencode", credential: { type: "api", key: "sk-oc" } }, + acc2: { id: "acc2", serviceID: "opencode-go", credential: { type: "api", key: "sk-oc-go" } }, + acc3: { id: "acc3", serviceID: "amazon-bedrock", credential: { type: "api", key: "aws-key" } }, + }, + active: { opencode: "acc1", "opencode-go": "acc2", "amazon-bedrock": "acc3" }, + }); + + // Only disconnect opencode — bedrock must survive + disconnectProviders(["opencode"], { accountJsonPath: accountPath, authJsonPath: "/nonexistent" }); + + const result = JSON.parse(fs.readFileSync(accountPath, "utf8")); + // opencode AND opencode-go removed (alias) + expect(result.accounts.acc1).toBeUndefined(); + expect(result.accounts.acc2).toBeUndefined(); + expect(result.active.opencode).toBeUndefined(); + expect(result.active["opencode-go"]).toBeUndefined(); + // amazon-bedrock PRESERVED + expect(result.accounts.acc3).toBeDefined(); + expect(result.accounts.acc3.serviceID).toBe("amazon-bedrock"); + expect(result.active["amazon-bedrock"]).toBe("acc3"); + }); }); From f02d77cf43702813a2971a6b0080ec75591aaf40 Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Thu, 20 Aug 2026 18:11:10 +0200 Subject: [PATCH 09/43] feat(onboarding): auto-send greeting to trigger overture after config (#449, TDD) After onboarding writes config and restarts the server, the next ChatPanel.openOrReveal posts a navigate message to the iframe: { source: 'amicode', kind: 'navigate', path: '/new-session?prompt=Hello&autoSend=1' } This creates a new session and auto-sends 'Hello', which triggers the overture interview skill (the agent detects a new user greeting and starts the onboarding interview). Implementation: - ChatPanel.pendingOnboardingGreeting (one-shot static flag) - ChatPanel.setPendingOnboardingGreeting() / clearPendingOnboardingGreeting() - postOnboardingGreeting() posts twice with delay (iframe mount timing) - Flag set in both config-success and confirm-import handlers - Flag consumed and cleared on next openOrReveal Tests (3, all TDD): - Posts navigate with autoSend=1 when flag is set - Does NOT post when flag is not set - Clears flag after first use (one-shot) --- packages/extension/src/chat_panel.ts | 35 ++++++++ packages/extension/src/onboarding_panel.ts | 5 ++ packages/extension/test/chat_panel.test.ts | 99 +++++++++++++++++++++- 3 files changed, 138 insertions(+), 1 deletion(-) diff --git a/packages/extension/src/chat_panel.ts b/packages/extension/src/chat_panel.ts index cd2fa2e0..bf9ced9d 100644 --- a/packages/extension/src/chat_panel.ts +++ b/packages/extension/src/chat_panel.ts @@ -48,6 +48,10 @@ export class ChatPanel { * staged skill set after every session prep; the composer button renders * only when the report-a-bug skill is there to answer it. */ private static bugReportAvailable = false; + /** One-shot flag: when true, the next openOrReveal posts a navigate message + * to start a new session with the onboarding greeting auto-sent. Cleared + * after use. Set by the onboarding panel after config-success/confirm-import. */ + private static pendingOnboardingGreeting = false; private readonly disposables: vscode.Disposable[] = []; /** Subscribe to live-panel count changes. Used by the workspace tree to mute the chat button. */ @@ -122,12 +126,35 @@ export class ChatPanel { setTimeout(() => void this.panel.webview.postMessage(envelope), 1500); } + /** Post a navigate message to open a new session with the onboarding + * greeting auto-sent. Delays to give a freshly-created iframe time to mount. + * The app's AmicodeNavigateBridge handles this. */ + private postOnboardingGreeting(): void { + const prompt = encodeURIComponent("Hello"); + const path = `/new-session?prompt=${prompt}&autoSend=1`; + const envelope = { source: "amicode", kind: "navigate", path }; + // Delay: iframe needs time to mount its listener + setTimeout(() => void this.panel.webview.postMessage(envelope), 2000); + setTimeout(() => void this.panel.webview.postMessage(envelope), 4000); + } + /** AC5's gate setter — called after each session prep with * bugReportSkillStaged(project.skillPaths). */ static setBugReportAvailable(available: boolean): void { ChatPanel.bugReportAvailable = available; } + /** Set by the onboarding panel after config is written — the next + * openOrReveal will post a navigate message to auto-send the greeting. */ + static setPendingOnboardingGreeting(pending: boolean): void { + ChatPanel.pendingOnboardingGreeting = pending; + } + + /** Clear the pending greeting flag (test cleanup / manual reset). */ + static clearPendingOnboardingGreeting(): void { + ChatPanel.pendingOnboardingGreeting = false; + } + /** The primary panel if one is live (never creates) — the down lane's * fallback when the server is mid-restart and no ready URL exists. */ static peek(): ChatPanel | undefined { @@ -193,9 +220,17 @@ export class ChatPanel { ): ChatPanel { if (ChatPanel.current) { ChatPanel.current.panel.reveal(vscode.ViewColumn.One); + if (ChatPanel.pendingOnboardingGreeting) { + ChatPanel.pendingOnboardingGreeting = false; + ChatPanel.current.postOnboardingGreeting(); + } return ChatPanel.current; } ChatPanel.current = ChatPanel.createPanel(ctx, vscode.ViewColumn.One, opencodeUrl, authToken, hideProjectDir); + if (ChatPanel.pendingOnboardingGreeting) { + ChatPanel.pendingOnboardingGreeting = false; + ChatPanel.current.postOnboardingGreeting(); + } return ChatPanel.current; } diff --git a/packages/extension/src/onboarding_panel.ts b/packages/extension/src/onboarding_panel.ts index 687707e5..87d682cb 100644 --- a/packages/extension/src/onboarding_panel.ts +++ b/packages/extension/src/onboarding_panel.ts @@ -21,6 +21,7 @@ import { isValidApiKey, type DetectedCredential, } from "./credential_scanner"; +import { ChatPanel } from "./chat_panel"; // ─── Provider → Model data (data-driven, not hard-coded conditionals) ──────── @@ -367,6 +368,8 @@ export function registerOnboardingPanel(ctx: vscode.ExtensionContext): void { writeOnboardingConfig(payload); panel.dispose(); fireOnboardingComplete(); + // Signal that the next chat panel open should auto-send the onboarding greeting + ChatPanel.setPendingOnboardingGreeting(true); // Restart server so it picks up the new provider config. // Chat opens via the onReady-gated listener in extension.ts. void vscode.commands.executeCommand("amicode.restartServer"); @@ -454,6 +457,8 @@ export function registerOnboardingPanel(ctx: vscode.ExtensionContext): void { testResults.clear(); panel.dispose(); fireOnboardingComplete(); + // Signal that the next chat panel open should auto-send the onboarding greeting + ChatPanel.setPendingOnboardingGreeting(true); // Restart server so it picks up the new provider config. // Chat opens via the onReady-gated listener in extension.ts. void vscode.commands.executeCommand("amicode.restartServer"); diff --git a/packages/extension/test/chat_panel.test.ts b/packages/extension/test/chat_panel.test.ts index 0b1744e0..4c11c1c2 100644 --- a/packages/extension/test/chat_panel.test.ts +++ b/packages/extension/test/chat_panel.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, afterEach } from "vitest"; +import { describe, it, expect, afterEach, vi } from "vitest"; import * as vscode from "vscode"; import { ChatPanel } from "../src/chat_panel"; import { mintServerPassword, serverAuthToken } from "../src/server_auth"; @@ -118,3 +118,100 @@ describe("ChatPanel — the amicode_bug_report boot param (amicode#250 AC5)", () expect(html).toContain('"close-bug-report"'); }); }); + +describe("ChatPanel — onboarding greeting auto-send (#449)", () => { + let restore: (() => void) | undefined; + let created: CapturedPanel[] = []; + afterEach(() => { + for (const p of created) p.dispose(); + restore?.(); + restore = undefined; + created = []; + ChatPanel.clearPendingOnboardingGreeting(); + }); + + it("posts a navigate message with auto-send greeting after onboarding completes", async () => { + const cap = capturePanel(); + restore = cap.restore; + created = cap.created; + + // Signal that onboarding just completed + ChatPanel.setPendingOnboardingGreeting(true); + + // Open the panel (simulates what happens after server restart) + ChatPanel.openOrReveal(fakeCtx(), new URL("http://127.0.0.1:43117/")); + + // Spy on postMessage + const messages: unknown[] = []; + const panel = cap.created[0] as unknown as { webview: { postMessage: (m: unknown) => Promise } }; + panel.webview.postMessage = (m: unknown) => { messages.push(m); return Promise.resolve(true); }; + + // Give the delayed postMessage time to fire + await new Promise((r) => setTimeout(r, 2200)); + + // Find the navigate message + const navigateMsg = messages.find( + (m) => (m as { source?: string; kind?: string }).source === "amicode" && (m as { kind?: string }).kind === "navigate", + ) as { source: string; kind: string; path: string } | undefined; + + expect(navigateMsg).toBeDefined(); + expect(navigateMsg!.path).toContain("/new-session"); + expect(navigateMsg!.path).toContain("autoSend=1"); + expect(navigateMsg!.path).toContain("prompt="); + }); + + it("does NOT post greeting when onboarding flag is not set", async () => { + const cap = capturePanel(); + restore = cap.restore; + created = cap.created; + + // No pending greeting flag + ChatPanel.openOrReveal(fakeCtx(), new URL("http://127.0.0.1:43117/")); + + const messages: unknown[] = []; + const panel = cap.created[0] as unknown as { webview: { postMessage: (m: unknown) => Promise } }; + panel.webview.postMessage = (m: unknown) => { messages.push(m); return Promise.resolve(true); }; + + await new Promise((r) => setTimeout(r, 2200)); + + const navigateMsg = messages.find( + (m) => (m as { source?: string; kind?: string }).source === "amicode" && (m as { kind?: string }).kind === "navigate", + ); + + expect(navigateMsg).toBeUndefined(); + }); + + it("clears the greeting flag after posting (one-shot)", async () => { + const cap = capturePanel(); + restore = cap.restore; + created = cap.created; + + ChatPanel.setPendingOnboardingGreeting(true); + ChatPanel.openOrReveal(fakeCtx(), new URL("http://127.0.0.1:43117/")); + + // Wait for the greeting to fire + await new Promise((r) => setTimeout(r, 2200)); + + // Dispose and re-create — second panel should NOT get the greeting + for (const p of created) p.dispose(); + created = []; + + const cap2 = capturePanel(); + restore = cap2.restore; + created = cap2.created; + + ChatPanel.openOrReveal(fakeCtx(), new URL("http://127.0.0.1:43117/")); + + const messages2: unknown[] = []; + const panel2 = cap2.created[0] as unknown as { webview: { postMessage: (m: unknown) => Promise } }; + panel2.webview.postMessage = (m: unknown) => { messages2.push(m); return Promise.resolve(true); }; + + await new Promise((r) => setTimeout(r, 2200)); + + const navigateMsg2 = messages2.find( + (m) => (m as { source?: string; kind?: string }).source === "amicode" && (m as { kind?: string }).kind === "navigate", + ); + + expect(navigateMsg2).toBeUndefined(); + }); +}); From fa125402c3f69a32601b5c44d70684215a2bed03 Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Thu, 20 Aug 2026 18:53:13 +0200 Subject: [PATCH 10/43] fix(onboarding): set greeting flag BEFORE fireOnboardingComplete The onOnboardingComplete listener (extension.ts:845) calls openOrReveal synchronously. If the flag is set AFTER the fire, the listener's openOrReveal runs first and sees pendingOnboardingGreeting=false. Fix: arm the flag before firing the event so the listener's openOrReveal consumes it correctly. --- packages/extension/src/onboarding_panel.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/extension/src/onboarding_panel.ts b/packages/extension/src/onboarding_panel.ts index 87d682cb..9095fd40 100644 --- a/packages/extension/src/onboarding_panel.ts +++ b/packages/extension/src/onboarding_panel.ts @@ -367,9 +367,9 @@ export function registerOnboardingPanel(ctx: vscode.ExtensionContext): void { const payload = msg.payload as OnboardingConfig; writeOnboardingConfig(payload); panel.dispose(); - fireOnboardingComplete(); // Signal that the next chat panel open should auto-send the onboarding greeting ChatPanel.setPendingOnboardingGreeting(true); + fireOnboardingComplete(); // Restart server so it picks up the new provider config. // Chat opens via the onReady-gated listener in extension.ts. void vscode.commands.executeCommand("amicode.restartServer"); @@ -456,9 +456,9 @@ export function registerOnboardingPanel(ctx: vscode.ExtensionContext): void { heldCredentials = []; testResults.clear(); panel.dispose(); - fireOnboardingComplete(); // Signal that the next chat panel open should auto-send the onboarding greeting ChatPanel.setPendingOnboardingGreeting(true); + fireOnboardingComplete(); // Restart server so it picks up the new provider config. // Chat opens via the onReady-gated listener in extension.ts. void vscode.commands.executeCommand("amicode.restartServer"); From 05acf801e7b83e3c7916e8206d2a5b2e61c1a833 Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Thu, 20 Aug 2026 19:02:36 +0200 Subject: [PATCH 11/43] =?UTF-8?q?fix(chat):=20add=20'navigate'=20to=20webv?= =?UTF-8?q?iew=E2=86=92iframe=20relay=20allowlist?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The postOnboardingGreeting message was being dropped because the webview's relay script only forwards messages with specific 'kind' values to the iframe's contentWindow. 'navigate' was not in the list. The AmicodeNavigateBridge in the app (app.tsx:458) listens for { source: 'amicode', kind: 'navigate', path: '...' } and creates a new session with the prompt — but it never received the message because the relay filtered it out. Added 'navigate' alongside the existing allowlisted kinds. --- packages/extension/src/chat_panel.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/extension/src/chat_panel.ts b/packages/extension/src/chat_panel.ts index bf9ced9d..d25a6f76 100644 --- a/packages/extension/src/chat_panel.ts +++ b/packages/extension/src/chat_panel.ts @@ -322,7 +322,7 @@ export class ChatPanel { // (webview-internal origin, never the opencode origin). Forward only // our own envelopes, pinned to the opencode origin. #351 adds // run:*/device:* envelopes for the Work Column inspector tabs. - if (d && d.source === "amicode" && (d.kind === "theme" || d.kind === "clipboard" || d.kind === "open-compute-connect" || d.kind === "open-bug-report" || d.kind === "close-bug-report" || d.kind === "dev-tools-status" || d.kind === "dev-tools-rebuild-status" || d.kind === "data-storage-defaults" || d.kind === "data-storage-status" || d.kind === "connections-credential-result" || d.kind === "connections-disconnect-result" || d.kind === "connections-revalidate-result" || d.kind === "connections-auth-result" || d.kind === "connections-choose-project-result" || d.kind === "connections-add-custom-result" || d.kind === "connections-remove-result" || (typeof d.kind === "string" && (d.kind.indexOf("run:") === 0 || d.kind.indexOf("device:") === 0)) || d.kind === "clipboard-image")) { + if (d && d.source === "amicode" && (d.kind === "theme" || d.kind === "clipboard" || d.kind === "navigate" || d.kind === "open-compute-connect" || d.kind === "open-bug-report" || d.kind === "close-bug-report" || d.kind === "dev-tools-status" || d.kind === "dev-tools-rebuild-status" || d.kind === "data-storage-defaults" || d.kind === "data-storage-status" || d.kind === "connections-credential-result" || d.kind === "connections-disconnect-result" || d.kind === "connections-revalidate-result" || d.kind === "connections-auth-result" || d.kind === "connections-choose-project-result" || d.kind === "connections-add-custom-result" || d.kind === "connections-remove-result" || (typeof d.kind === "string" && (d.kind.indexOf("run:") === 0 || d.kind.indexOf("device:") === 0)) || d.kind === "clipboard-image")) { var f = document.querySelector("iframe"); if (f && f.contentWindow) f.contentWindow.postMessage(d, ${origin}); } From 95049ad5e6c60cc9887b754f710ab7f9e2f9213b Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Thu, 20 Aug 2026 19:08:13 +0200 Subject: [PATCH 12/43] fix(onboarding): send 'Begin onboarding' as auto-submit prompt Changed from 'Hello' (which just sat in the textbox) to 'Begin onboarding' which triggers the overture skill to start the interview. The autoSend=1 flag tells the app's draft controller to submit automatically on mount. --- packages/extension/src/chat_panel.ts | 2 +- packages/extension/test/chat_panel.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/extension/src/chat_panel.ts b/packages/extension/src/chat_panel.ts index d25a6f76..8aebc4fb 100644 --- a/packages/extension/src/chat_panel.ts +++ b/packages/extension/src/chat_panel.ts @@ -130,7 +130,7 @@ export class ChatPanel { * greeting auto-sent. Delays to give a freshly-created iframe time to mount. * The app's AmicodeNavigateBridge handles this. */ private postOnboardingGreeting(): void { - const prompt = encodeURIComponent("Hello"); + const prompt = encodeURIComponent("Begin onboarding"); const path = `/new-session?prompt=${prompt}&autoSend=1`; const envelope = { source: "amicode", kind: "navigate", path }; // Delay: iframe needs time to mount its listener diff --git a/packages/extension/test/chat_panel.test.ts b/packages/extension/test/chat_panel.test.ts index 4c11c1c2..b459022c 100644 --- a/packages/extension/test/chat_panel.test.ts +++ b/packages/extension/test/chat_panel.test.ts @@ -157,7 +157,7 @@ describe("ChatPanel — onboarding greeting auto-send (#449)", () => { expect(navigateMsg).toBeDefined(); expect(navigateMsg!.path).toContain("/new-session"); expect(navigateMsg!.path).toContain("autoSend=1"); - expect(navigateMsg!.path).toContain("prompt="); + expect(navigateMsg!.path).toContain("prompt=" + encodeURIComponent("Begin onboarding")); }); it("does NOT post greeting when onboarding flag is not set", async () => { From 585da9280d27ba8370837b050d88da0b349a0b58 Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Thu, 20 Aug 2026 19:24:20 +0200 Subject: [PATCH 13/43] feat(onboarding): arm session via server API to bypass UI model gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The navigate+autoSend approach was blocked by the app's model selection popup (requires clicking a model before first submit). The bug reporter avoids this by creating+arming sessions directly via the server API. Now after server restart, if onboarding just completed: 1. POST /session creates the session (server-side, no UI) 2. POST /session/:id/command arms it with 'Begin onboarding' 3. postOnboardingGreeting() navigates the iframe to show it The server resolves its own default model — no UI gate. Also: made postOnboardingGreeting() public, added consumePendingOnboardingGreeting() for explicit control flow, and moved consumption out of openOrReveal into extension.ts. --- packages/extension/src/chat_panel.ts | 19 ++++---- packages/extension/src/extension.ts | 48 +++++++++++++++++++- packages/extension/test/chat_panel.test.ts | 52 +++++----------------- 3 files changed, 68 insertions(+), 51 deletions(-) diff --git a/packages/extension/src/chat_panel.ts b/packages/extension/src/chat_panel.ts index 8aebc4fb..b18a72ec 100644 --- a/packages/extension/src/chat_panel.ts +++ b/packages/extension/src/chat_panel.ts @@ -127,9 +127,9 @@ export class ChatPanel { } /** Post a navigate message to open a new session with the onboarding - * greeting auto-sent. Delays to give a freshly-created iframe time to mount. + * prompt auto-sent. Delays to give a freshly-created iframe time to mount. * The app's AmicodeNavigateBridge handles this. */ - private postOnboardingGreeting(): void { + postOnboardingGreeting(): void { const prompt = encodeURIComponent("Begin onboarding"); const path = `/new-session?prompt=${prompt}&autoSend=1`; const envelope = { source: "amicode", kind: "navigate", path }; @@ -155,6 +155,13 @@ export class ChatPanel { ChatPanel.pendingOnboardingGreeting = false; } + /** Consume and clear the pending greeting flag. Returns true if it was set. */ + static consumePendingOnboardingGreeting(): boolean { + if (!ChatPanel.pendingOnboardingGreeting) return false; + ChatPanel.pendingOnboardingGreeting = false; + return true; + } + /** The primary panel if one is live (never creates) — the down lane's * fallback when the server is mid-restart and no ready URL exists. */ static peek(): ChatPanel | undefined { @@ -220,17 +227,9 @@ export class ChatPanel { ): ChatPanel { if (ChatPanel.current) { ChatPanel.current.panel.reveal(vscode.ViewColumn.One); - if (ChatPanel.pendingOnboardingGreeting) { - ChatPanel.pendingOnboardingGreeting = false; - ChatPanel.current.postOnboardingGreeting(); - } return ChatPanel.current; } ChatPanel.current = ChatPanel.createPanel(ctx, vscode.ViewColumn.One, opencodeUrl, authToken, hideProjectDir); - if (ChatPanel.pendingOnboardingGreeting) { - ChatPanel.pendingOnboardingGreeting = false; - ChatPanel.current.postOnboardingGreeting(); - } return ChatPanel.current; } diff --git a/packages/extension/src/extension.ts b/packages/extension/src/extension.ts index 6fd5a4a2..f56a054c 100644 --- a/packages/extension/src/extension.ts +++ b/packages/extension/src/extension.ts @@ -207,6 +207,39 @@ async function refreshDeviceInspector(channel: vscode.OutputChannel): Promise(); +/** Create a session and arm it with "Begin onboarding" via the server API. + * Bypasses the UI model gate (the server resolves its own default model). + * Returns the session ID on success, undefined on failure. */ +async function armOnboardingSession( + serverUrl: URL, + authHeaders: Record, + projectDir?: string, +): Promise { + try { + const collectionUrl = new URL("/session", serverUrl); + if (projectDir) collectionUrl.searchParams.set("directory", projectDir); + const createRes = await fetch(collectionUrl.toString(), { + method: "POST", + headers: { "Content-Type": "application/json", ...authHeaders }, + body: JSON.stringify({ title: "Onboarding" }), + }); + if (!createRes.ok) return undefined; + const { id } = (await createRes.json()) as { id?: string }; + if (!id) return undefined; + + const commandUrl = new URL(`/session/${id}/command`, serverUrl); + const commandRes = await fetch(commandUrl.toString(), { + method: "POST", + headers: { "Content-Type": "application/json", ...authHeaders }, + body: JSON.stringify({ command: "Begin onboarding", arguments: "" }), + }); + if (!commandRes.ok) return undefined; + return id; + } catch { + return undefined; + } +} + export async function activate(ctx: vscode.ExtensionContext): Promise { const opencodeChannel = vscode.window.createOutputChannel("Amicode — opencode"); const runsChannel = vscode.window.createOutputChannel("Amicode — runs"); @@ -821,7 +854,20 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { }); } else if (vscode.workspace.getConfiguration("amicode").get("chat.autoOpen", true)) { // Normal path: model configured → open chat directly - ChatPanel.openOrReveal(ctx, url, serverAuthToken(serverPassword), opencodeProject.projectDir); + const panel = ChatPanel.openOrReveal(ctx, url, serverAuthToken(serverPassword), opencodeProject.projectDir); + // Post-onboarding: create a session and arm it with "Begin onboarding" + // via the server API (bypasses the UI model gate), then navigate to it. + if (ChatPanel.consumePendingOnboardingGreeting()) { + void armOnboardingSession(url, serverAuthHeaders, opencodeProject.projectDir).then((sessionID) => { + if (sessionID) { + // Navigate the app to the armed session + panel.postOnboardingGreeting(); + } + }).catch(() => { + // Fallback: just navigate with prompt (may hit model gate) + panel.postOnboardingGreeting(); + }); + } } // Surface ONE explicit LLM-provider signal at boot, read from opencode's // OWN resolution (its live /config/providers) — not a silent hang at the diff --git a/packages/extension/test/chat_panel.test.ts b/packages/extension/test/chat_panel.test.ts index b459022c..01ecc349 100644 --- a/packages/extension/test/chat_panel.test.ts +++ b/packages/extension/test/chat_panel.test.ts @@ -135,16 +135,16 @@ describe("ChatPanel — onboarding greeting auto-send (#449)", () => { restore = cap.restore; created = cap.created; - // Signal that onboarding just completed - ChatPanel.setPendingOnboardingGreeting(true); - - // Open the panel (simulates what happens after server restart) - ChatPanel.openOrReveal(fakeCtx(), new URL("http://127.0.0.1:43117/")); + // Open the panel and explicitly post the greeting (extension.ts does this after arming) + const panel = ChatPanel.openOrReveal(fakeCtx(), new URL("http://127.0.0.1:43117/")); // Spy on postMessage const messages: unknown[] = []; - const panel = cap.created[0] as unknown as { webview: { postMessage: (m: unknown) => Promise } }; - panel.webview.postMessage = (m: unknown) => { messages.push(m); return Promise.resolve(true); }; + const webview = cap.created[0] as unknown as { webview: { postMessage: (m: unknown) => Promise } }; + webview.webview.postMessage = (m: unknown) => { messages.push(m); return Promise.resolve(true); }; + + // Call postOnboardingGreeting (what extension.ts does after armOnboardingSession) + panel.postOnboardingGreeting(); // Give the delayed postMessage time to fire await new Promise((r) => setTimeout(r, 2200)); @@ -160,12 +160,12 @@ describe("ChatPanel — onboarding greeting auto-send (#449)", () => { expect(navigateMsg!.path).toContain("prompt=" + encodeURIComponent("Begin onboarding")); }); - it("does NOT post greeting when onboarding flag is not set", async () => { + it("does NOT post greeting when postOnboardingGreeting is not called", async () => { const cap = capturePanel(); restore = cap.restore; created = cap.created; - // No pending greeting flag + // Open panel without calling postOnboardingGreeting ChatPanel.openOrReveal(fakeCtx(), new URL("http://127.0.0.1:43117/")); const messages: unknown[] = []; @@ -181,37 +181,9 @@ describe("ChatPanel — onboarding greeting auto-send (#449)", () => { expect(navigateMsg).toBeUndefined(); }); - it("clears the greeting flag after posting (one-shot)", async () => { - const cap = capturePanel(); - restore = cap.restore; - created = cap.created; - + it("consumePendingOnboardingGreeting returns true once then false", () => { ChatPanel.setPendingOnboardingGreeting(true); - ChatPanel.openOrReveal(fakeCtx(), new URL("http://127.0.0.1:43117/")); - - // Wait for the greeting to fire - await new Promise((r) => setTimeout(r, 2200)); - - // Dispose and re-create — second panel should NOT get the greeting - for (const p of created) p.dispose(); - created = []; - - const cap2 = capturePanel(); - restore = cap2.restore; - created = cap2.created; - - ChatPanel.openOrReveal(fakeCtx(), new URL("http://127.0.0.1:43117/")); - - const messages2: unknown[] = []; - const panel2 = cap2.created[0] as unknown as { webview: { postMessage: (m: unknown) => Promise } }; - panel2.webview.postMessage = (m: unknown) => { messages2.push(m); return Promise.resolve(true); }; - - await new Promise((r) => setTimeout(r, 2200)); - - const navigateMsg2 = messages2.find( - (m) => (m as { source?: string; kind?: string }).source === "amicode" && (m as { kind?: string }).kind === "navigate", - ); - - expect(navigateMsg2).toBeUndefined(); + expect(ChatPanel.consumePendingOnboardingGreeting()).toBe(true); + expect(ChatPanel.consumePendingOnboardingGreeting()).toBe(false); }); }); From cf98f7ca01a1764f501f3d6bfa726c558b427db6 Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Thu, 20 Aug 2026 19:27:06 +0200 Subject: [PATCH 14/43] fix(onboarding): drop navigate fallback, rely on SSE session sync The server-side session (created via POST /session + POST /session/:id/command) appears in the app's session list via SSE sync automatically. The navigate message was redundant and hit the model gate. Now we just create+arm the session and let the app's real-time sync surface it. --- packages/extension/src/extension.ts | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/packages/extension/src/extension.ts b/packages/extension/src/extension.ts index f56a054c..5402f74f 100644 --- a/packages/extension/src/extension.ts +++ b/packages/extension/src/extension.ts @@ -856,17 +856,10 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { // Normal path: model configured → open chat directly const panel = ChatPanel.openOrReveal(ctx, url, serverAuthToken(serverPassword), opencodeProject.projectDir); // Post-onboarding: create a session and arm it with "Begin onboarding" - // via the server API (bypasses the UI model gate), then navigate to it. + // via the server API (bypasses the UI model gate). The session appears + // in the app's session list automatically via SSE sync. if (ChatPanel.consumePendingOnboardingGreeting()) { - void armOnboardingSession(url, serverAuthHeaders, opencodeProject.projectDir).then((sessionID) => { - if (sessionID) { - // Navigate the app to the armed session - panel.postOnboardingGreeting(); - } - }).catch(() => { - // Fallback: just navigate with prompt (may hit model gate) - panel.postOnboardingGreeting(); - }); + void armOnboardingSession(url, serverAuthHeaders, opencodeProject.projectDir); } } // Surface ONE explicit LLM-provider signal at boot, read from opencode's From 6232ff6b5144fab4fcb5726c41113933f2b38966 Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Thu, 20 Aug 2026 19:32:28 +0200 Subject: [PATCH 15/43] feat(onboarding): navigate to armed session by ID after creation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After armOnboardingSession creates+arms the session via server API, post a navigate message with path=/session/ to the app. The AmicodeNavigateBridge (opencode fork) now handles /session/:id by calling tabs.openPath with activate:true — opening the session tab front and center. Also exposed ChatPanel.postMessage() for arbitrary envelope posting. --- packages/extension/src/chat_panel.ts | 5 +++++ packages/extension/src/extension.ts | 13 ++++++++++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/packages/extension/src/chat_panel.ts b/packages/extension/src/chat_panel.ts index b18a72ec..ae5c76ae 100644 --- a/packages/extension/src/chat_panel.ts +++ b/packages/extension/src/chat_panel.ts @@ -138,6 +138,11 @@ export class ChatPanel { setTimeout(() => void this.panel.webview.postMessage(envelope), 4000); } + /** Post an arbitrary message to the webview (relayed to the iframe). */ + postMessage(msg: unknown): Promise { + return this.panel.webview.postMessage(msg); + } + /** AC5's gate setter — called after each session prep with * bugReportSkillStaged(project.skillPaths). */ static setBugReportAvailable(available: boolean): void { diff --git a/packages/extension/src/extension.ts b/packages/extension/src/extension.ts index 5402f74f..128b41f5 100644 --- a/packages/extension/src/extension.ts +++ b/packages/extension/src/extension.ts @@ -856,10 +856,17 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { // Normal path: model configured → open chat directly const panel = ChatPanel.openOrReveal(ctx, url, serverAuthToken(serverPassword), opencodeProject.projectDir); // Post-onboarding: create a session and arm it with "Begin onboarding" - // via the server API (bypasses the UI model gate). The session appears - // in the app's session list automatically via SSE sync. + // via the server API (bypasses the UI model gate), then navigate to it. if (ChatPanel.consumePendingOnboardingGreeting()) { - void armOnboardingSession(url, serverAuthHeaders, opencodeProject.projectDir); + void armOnboardingSession(url, serverAuthHeaders, opencodeProject.projectDir).then((sessionID) => { + if (sessionID) { + // Navigate the app to the armed session + const path = `/session/${sessionID}`; + const envelope = { source: "amicode", kind: "navigate", path }; + setTimeout(() => void panel.postMessage(envelope), 2000); + setTimeout(() => void panel.postMessage(envelope), 4000); + } + }); } } // Surface ONE explicit LLM-provider signal at boot, read from opencode's From d5fa7db635e9dcfb8993969899b5e9bf39c34486 Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Thu, 20 Aug 2026 19:37:03 +0200 Subject: [PATCH 16/43] fix(onboarding): use openNew + navigate pattern (matches fleet approach) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The server-API approach (armOnboardingSession + navigate to /session/:id) wasn't working — the session existed but the app couldn't navigate to it reliably (sync race). Switch to the exact pattern that works for fleet: open a NEW panel with the iframe URL pointing to /new-session, then postOnboardingGreeting() sends the navigate message with autoSend=1. The iframe boots directly on the draft page and is ready to receive the prompt immediately. This matches launchFleetChat() from the #363 fleet branch exactly. --- packages/extension/src/extension.ts | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/packages/extension/src/extension.ts b/packages/extension/src/extension.ts index 128b41f5..fbf1eb87 100644 --- a/packages/extension/src/extension.ts +++ b/packages/extension/src/extension.ts @@ -854,19 +854,15 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { }); } else if (vscode.workspace.getConfiguration("amicode").get("chat.autoOpen", true)) { // Normal path: model configured → open chat directly - const panel = ChatPanel.openOrReveal(ctx, url, serverAuthToken(serverPassword), opencodeProject.projectDir); - // Post-onboarding: create a session and arm it with "Begin onboarding" - // via the server API (bypasses the UI model gate), then navigate to it. + ChatPanel.openOrReveal(ctx, url, serverAuthToken(serverPassword), opencodeProject.projectDir); + // Post-onboarding: open a NEW panel on /new-session and auto-send + // "Begin onboarding" via the navigate bridge (same pattern as fleet). if (ChatPanel.consumePendingOnboardingGreeting()) { - void armOnboardingSession(url, serverAuthHeaders, opencodeProject.projectDir).then((sessionID) => { - if (sessionID) { - // Navigate the app to the armed session - const path = `/session/${sessionID}`; - const envelope = { source: "amicode", kind: "navigate", path }; - setTimeout(() => void panel.postMessage(envelope), 2000); - setTimeout(() => void panel.postMessage(envelope), 4000); - } - }); + const draftUrl = new URL(url.href); + draftUrl.pathname = "/new-session"; + draftUrl.search = ""; + const onboardPanel = ChatPanel.openNew(ctx, draftUrl, serverAuthToken(serverPassword), opencodeProject.projectDir); + onboardPanel.postOnboardingGreeting(); } } // Surface ONE explicit LLM-provider signal at boot, read from opencode's From 2afbe2265f15be1eb1177284088fbc32bad850d8 Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Thu, 20 Aug 2026 19:40:59 +0200 Subject: [PATCH 17/43] fix(onboarding): use existing panel instead of opening a second tab MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit openNew was creating 'Amicode Chat 2'. Instead, use the existing panel from openOrReveal and post the navigate message into it — the app's AmicodeNavigateBridge creates a new draft tab WITHIN that panel. --- packages/extension/src/extension.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/extension/src/extension.ts b/packages/extension/src/extension.ts index fbf1eb87..56ff2dd7 100644 --- a/packages/extension/src/extension.ts +++ b/packages/extension/src/extension.ts @@ -854,15 +854,11 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { }); } else if (vscode.workspace.getConfiguration("amicode").get("chat.autoOpen", true)) { // Normal path: model configured → open chat directly - ChatPanel.openOrReveal(ctx, url, serverAuthToken(serverPassword), opencodeProject.projectDir); - // Post-onboarding: open a NEW panel on /new-session and auto-send - // "Begin onboarding" via the navigate bridge (same pattern as fleet). + const panel = ChatPanel.openOrReveal(ctx, url, serverAuthToken(serverPassword), opencodeProject.projectDir); + // Post-onboarding: send navigate message to the existing panel to + // start a new session with "Begin onboarding" auto-sent. if (ChatPanel.consumePendingOnboardingGreeting()) { - const draftUrl = new URL(url.href); - draftUrl.pathname = "/new-session"; - draftUrl.search = ""; - const onboardPanel = ChatPanel.openNew(ctx, draftUrl, serverAuthToken(serverPassword), opencodeProject.projectDir); - onboardPanel.postOnboardingGreeting(); + panel.postOnboardingGreeting(); } } // Surface ONE explicit LLM-provider signal at boot, read from opencode's From dac6b15b71efe35f05eea17c11093a4a8df1952e Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Thu, 20 Aug 2026 22:09:41 +0200 Subject: [PATCH 18/43] feat(onboarding): transition splash between provider setup and chat session MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Instead of disposing the onboarding panel immediately on confirm-import (leaving dead air while the server restarts), the panel stays alive as a transition splash showing the Amico idle animation + 'Getting Amico ready...' The navigate message is now event-driven: posted only after the app signals ready (app-ready message from iframe), with a 10s timeout fallback. This replaces the blind 2000ms/4000ms setTimeout. Flow: confirm → show-transition → server restart → chat panel opens → app-ready fires → navigate posted + onboarding panel dismissed. New public API: - dismissOnboardingPanel() — extension calls after app-ready - ChatPanel.onAppReady(cb) — one-shot callback on app-ready message - postOnboardingGreeting(timeoutMs) — event-driven with fallback 89 tests pass (36 onboarding + 11 chat + 42 credential). --- packages/extension/src/chat_panel.ts | 44 ++++++++-- packages/extension/src/extension.ts | 13 ++- packages/extension/src/onboarding_panel.ts | 19 +++- packages/extension/src/onboarding_webview.ts | 29 +++++++ packages/extension/test/chat_panel.test.ts | 86 ++++++++++++++++--- .../extension/test/onboarding_panel.test.ts | 55 +++++++++++- 6 files changed, 217 insertions(+), 29 deletions(-) diff --git a/packages/extension/src/chat_panel.ts b/packages/extension/src/chat_panel.ts index ae5c76ae..7dc2e266 100644 --- a/packages/extension/src/chat_panel.ts +++ b/packages/extension/src/chat_panel.ts @@ -52,6 +52,8 @@ export class ChatPanel { * to start a new session with the onboarding greeting auto-sent. Cleared * after use. Set by the onboarding panel after config-success/confirm-import. */ private static pendingOnboardingGreeting = false; + /** Callbacks fired when the app signals ready (app-ready message from iframe). */ + private static appReadyCallbacks: Array<() => void> = []; private readonly disposables: vscode.Disposable[] = []; /** Subscribe to live-panel count changes. Used by the workspace tree to mute the chat button. */ @@ -92,6 +94,14 @@ export class ChatPanel { const serverUrl = opencodeUrl.origin; this.panel.webview.onDidReceiveMessage( (msg) => { + // app-ready: the SolidJS app has mounted and is rendering. Fire + // any registered callbacks (one-shot) and clear the list. + if (msg && msg.source === "amicode" && msg.kind === "app-ready") { + const cbs = ChatPanel.appReadyCallbacks.slice(); + ChatPanel.appReadyCallbacks = []; + for (const cb of cbs) cb(); + return; + } // iframe → extension bridge: the outer webview relay (renderHtml) // forwards the framed app's envelopes here; the shared handler owns the // strict allowlists (chat_bridge.ts, also used by the deck's panes). @@ -127,15 +137,23 @@ export class ChatPanel { } /** Post a navigate message to open a new session with the onboarding - * prompt auto-sent. Delays to give a freshly-created iframe time to mount. - * The app's AmicodeNavigateBridge handles this. */ - postOnboardingGreeting(): void { + * prompt auto-sent. Waits for the app-ready signal before posting (the app + * must be mounted to handle the navigate). Falls back to a timeout if + * app-ready never fires. */ + postOnboardingGreeting(timeoutMs = 10_000): void { const prompt = encodeURIComponent("Begin onboarding"); const path = `/new-session?prompt=${prompt}&autoSend=1`; const envelope = { source: "amicode", kind: "navigate", path }; - // Delay: iframe needs time to mount its listener - setTimeout(() => void this.panel.webview.postMessage(envelope), 2000); - setTimeout(() => void this.panel.webview.postMessage(envelope), 4000); + let sent = false; + const send = () => { + if (sent) return; + sent = true; + void this.panel.webview.postMessage(envelope); + }; + ChatPanel.onAppReady(send); + // Fallback: if app-ready never fires (server hung, iframe broken), + // post after timeout so the user isn't stuck on the splash forever. + setTimeout(send, timeoutMs); } /** Post an arbitrary message to the webview (relayed to the iframe). */ @@ -160,6 +178,18 @@ export class ChatPanel { ChatPanel.pendingOnboardingGreeting = false; } + /** Register a one-shot callback for when the app signals ready. + * All registered callbacks fire once on the first app-ready message, + * then the list is cleared. */ + static onAppReady(cb: () => void): void { + ChatPanel.appReadyCallbacks.push(cb); + } + + /** Clear app-ready callbacks (test cleanup). */ + static clearAppReadyCallbacks(): void { + ChatPanel.appReadyCallbacks = []; + } + /** Consume and clear the pending greeting flag. Returns true if it was set. */ static consumePendingOnboardingGreeting(): boolean { if (!ChatPanel.pendingOnboardingGreeting) return false; @@ -317,7 +347,7 @@ export class ChatPanel { replyClipboardImage(d.nonce); return; } - if (d && d.source === "amicode" && (d.kind === "command" || d.kind === "clipboard-request" || d.kind === "clipboard-write" || d.kind === "open-external" || d.kind === "open-file" || d.kind === "save-file" || d.kind === "set-default-model" || d.kind === "bug-filed" || d.kind === "bug-report-closed" || d.kind === "bug-report-poke" || d.kind === "dev-tools-update" || d.kind === "dev-tools-rebuild" || d.kind === "data-storage-query" || d.kind === "data-storage-update" || d.kind === "redo-onboarding" || d.kind === "device:refresh" || d.kind === "connections-credential" || d.kind === "connections-disconnect" || d.kind === "connections-revalidate" || d.kind === "connections-auth" || d.kind === "connections-choose-project" || d.kind === "connections-add-custom" || d.kind === "connections-remove")) { + if (d && d.source === "amicode" && (d.kind === "command" || d.kind === "clipboard-request" || d.kind === "clipboard-write" || d.kind === "open-external" || d.kind === "open-file" || d.kind === "save-file" || d.kind === "set-default-model" || d.kind === "bug-filed" || d.kind === "bug-report-closed" || d.kind === "bug-report-poke" || d.kind === "dev-tools-update" || d.kind === "dev-tools-rebuild" || d.kind === "data-storage-query" || d.kind === "data-storage-update" || d.kind === "redo-onboarding" || d.kind === "device:refresh" || d.kind === "connections-credential" || d.kind === "connections-disconnect" || d.kind === "connections-revalidate" || d.kind === "connections-auth" || d.kind === "connections-choose-project" || d.kind === "connections-add-custom" || d.kind === "connections-remove" || d.kind === "app-ready")) { vscode.postMessage(d); } return; diff --git a/packages/extension/src/extension.ts b/packages/extension/src/extension.ts index 56ff2dd7..a9c7c889 100644 --- a/packages/extension/src/extension.ts +++ b/packages/extension/src/extension.ts @@ -38,8 +38,8 @@ import { writeStopFile, savePulseTo, stopPlan, forceStop, runLogMtime } from "./ import { watchSolverMode, applyEntitlementForMode, readSolverModeState } from "./solver_mode"; import { runSetCloudKeyCommand } from "./cloud_key"; import { amicodeOpsDir } from "./substrate/vault_store"; -import { registerOnboardingPanel, onOnboardingComplete, onOnboardingCancelled } from "./onboarding_panel"; -import { isModelConfigured } from "./onboarding_routing"; +import { registerOnboardingPanel, onOnboardingComplete, onOnboardingCancelled, dismissOnboardingPanel } from "./onboarding_panel"; +import { isModelConfigured, writeWelcomeShown } from "./onboarding_routing"; import { stagePasqalConnector } from "./pasqal_assets"; import { needsProvision, pasqalVenvDir, provisionPasqalPython } from "./pasqal_python"; import { createLocalPersonalVault, sanitizeVaultName, suggestVaultName } from "./substrate/vault_setup"; @@ -856,8 +856,15 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { // Normal path: model configured → open chat directly const panel = ChatPanel.openOrReveal(ctx, url, serverAuthToken(serverPassword), opencodeProject.projectDir); // Post-onboarding: send navigate message to the existing panel to - // start a new session with "Begin onboarding" auto-sent. + // start a new session with "Begin onboarding" auto-sent. The navigate + // message fires event-driven (on app-ready), not on a blind timer. + // Also dismiss the onboarding splash panel once the app is ready. if (ChatPanel.consumePendingOnboardingGreeting()) { + let dismissed = false; + const dismiss = () => { if (!dismissed) { dismissed = true; dismissOnboardingPanel(); } }; + ChatPanel.onAppReady(dismiss); + // Safety: force-dismiss after 10s if app-ready never fires. + setTimeout(dismiss, 10_000); panel.postOnboardingGreeting(); } } diff --git a/packages/extension/src/onboarding_panel.ts b/packages/extension/src/onboarding_panel.ts index 9095fd40..64f6ab8b 100644 --- a/packages/extension/src/onboarding_panel.ts +++ b/packages/extension/src/onboarding_panel.ts @@ -329,6 +329,14 @@ export function _resetForTesting(): void { currentPanel = undefined; } +/** Dismiss the onboarding panel (dispose it). Called by the extension host + * after the chat panel's app signals ready — ends the transition splash. */ +export function dismissOnboardingPanel(): void { + if (currentPanel) { + currentPanel.dispose(); + } +} + /** Register the onboarding panel command. Call from extension.ts activate(). */ export function registerOnboardingPanel(ctx: vscode.ExtensionContext): void { ctx.subscriptions.push( @@ -455,13 +463,19 @@ export function registerOnboardingPanel(ctx: vscode.ExtensionContext): void { } heldCredentials = []; testResults.clear(); - panel.dispose(); + // Keep the panel alive as a transition splash — tell the webview to + // show the "Getting Amico ready..." state instead of disposing now. + void panel.webview.postMessage({ type: "show-transition" }); // Signal that the next chat panel open should auto-send the onboarding greeting ChatPanel.setPendingOnboardingGreeting(true); fireOnboardingComplete(); // Restart server so it picks up the new provider config. // Chat opens via the onReady-gated listener in extension.ts. void vscode.commands.executeCommand("amicode.restartServer"); + } else if (msg.type === "transition-complete") { + // The extension signals that the chat panel is ready — dispose the + // splash now. This is posted by the extension host after app-ready. + panel.dispose(); } }, null, @@ -502,7 +516,7 @@ function buildWebviewHtml( diff --git a/packages/extension/src/onboarding_webview.ts b/packages/extension/src/onboarding_webview.ts index 36c50ff0..b19e9306 100644 --- a/packages/extension/src/onboarding_webview.ts +++ b/packages/extension/src/onboarding_webview.ts @@ -900,6 +900,35 @@ function buildForm(): void { // ─── Boot ──────────────────────────────────────────────────────────────────── +// Listen for the transition-state signal from the host (after confirm-import). +// Hides the form, keeps the animation (Amico idle), and shows "Getting Amico ready..." +window.addEventListener("message", (event) => { + const msg = event.data; + if (msg?.type === "show-transition") { + // Hide the form + formEl.classList.remove("visible"); + formEl.style.display = "none"; + // Hide the cancel button + const cancelEl = document.getElementById("cancel-btn"); + if (cancelEl) cancelEl.style.display = "none"; + // Show the animation container (it may already be visible if animation played) + animationEl.style.display = "flex"; + // Add "Getting Amico ready..." text below the animation + let transitionText = document.getElementById("transition-text"); + if (!transitionText) { + transitionText = document.createElement("div"); + transitionText.id = "transition-text"; + transitionText.style.cssText = ` + text-align: center; margin-top: 24px; font-size: 14px; + color: var(--vscode-descriptionForeground, #999); + animation: fadeIn 0.4s ease-out; + `; + transitionText.textContent = "Getting Amico ready..."; + animationEl.parentElement!.insertBefore(transitionText, animationEl.nextSibling); + } + } +}); + // Wire the cancel button const cancelBtn = document.getElementById("cancel-btn"); if (cancelBtn) { diff --git a/packages/extension/test/chat_panel.test.ts b/packages/extension/test/chat_panel.test.ts index 01ecc349..4eaa92f2 100644 --- a/packages/extension/test/chat_panel.test.ts +++ b/packages/extension/test/chat_panel.test.ts @@ -128,28 +128,34 @@ describe("ChatPanel — onboarding greeting auto-send (#449)", () => { restore = undefined; created = []; ChatPanel.clearPendingOnboardingGreeting(); + ChatPanel.clearAppReadyCallbacks(); }); - it("posts a navigate message with auto-send greeting after onboarding completes", async () => { + it("posts the navigate message only AFTER app-ready fires (event-driven, not blind timer)", async () => { const cap = capturePanel(); restore = cap.restore; created = cap.created; - // Open the panel and explicitly post the greeting (extension.ts does this after arming) const panel = ChatPanel.openOrReveal(fakeCtx(), new URL("http://127.0.0.1:43117/")); - // Spy on postMessage const messages: unknown[] = []; - const webview = cap.created[0] as unknown as { webview: { postMessage: (m: unknown) => Promise } }; + const webview = cap.created[0] as unknown as { webview: { postMessage: (m: unknown) => Promise; _simulateMessage: (msg: unknown) => void } }; webview.webview.postMessage = (m: unknown) => { messages.push(m); return Promise.resolve(true); }; - // Call postOnboardingGreeting (what extension.ts does after armOnboardingSession) + // Call postOnboardingGreeting — should NOT post immediately panel.postOnboardingGreeting(); - // Give the delayed postMessage time to fire - await new Promise((r) => setTimeout(r, 2200)); + // Wait a tick — no navigate message yet (no blind timer should fire this fast) + await new Promise((r) => setTimeout(r, 50)); + const earlyNavigate = messages.find( + (m) => (m as { kind?: string }).kind === "navigate", + ); + expect(earlyNavigate).toBeUndefined(); + + // Now simulate app-ready — the message should fire + webview.webview._simulateMessage({ source: "amicode", kind: "app-ready" }); + await new Promise((r) => setTimeout(r, 50)); - // Find the navigate message const navigateMsg = messages.find( (m) => (m as { source?: string; kind?: string }).source === "amicode" && (m as { kind?: string }).kind === "navigate", ) as { source: string; kind: string; path: string } | undefined; @@ -160,24 +166,25 @@ describe("ChatPanel — onboarding greeting auto-send (#449)", () => { expect(navigateMsg!.path).toContain("prompt=" + encodeURIComponent("Begin onboarding")); }); - it("does NOT post greeting when postOnboardingGreeting is not called", async () => { + it("does NOT post navigate when postOnboardingGreeting was not called (even after app-ready)", async () => { const cap = capturePanel(); restore = cap.restore; created = cap.created; - // Open panel without calling postOnboardingGreeting + // Open panel WITHOUT calling postOnboardingGreeting ChatPanel.openOrReveal(fakeCtx(), new URL("http://127.0.0.1:43117/")); const messages: unknown[] = []; - const panel = cap.created[0] as unknown as { webview: { postMessage: (m: unknown) => Promise } }; - panel.webview.postMessage = (m: unknown) => { messages.push(m); return Promise.resolve(true); }; + const webview = cap.created[0] as unknown as { webview: { postMessage: (m: unknown) => Promise; _simulateMessage: (msg: unknown) => void } }; + webview.webview.postMessage = (m: unknown) => { messages.push(m); return Promise.resolve(true); }; - await new Promise((r) => setTimeout(r, 2200)); + // Simulate app-ready + webview.webview._simulateMessage({ source: "amicode", kind: "app-ready" }); + await new Promise((r) => setTimeout(r, 50)); const navigateMsg = messages.find( (m) => (m as { source?: string; kind?: string }).source === "amicode" && (m as { kind?: string }).kind === "navigate", ); - expect(navigateMsg).toBeUndefined(); }); @@ -186,4 +193,55 @@ describe("ChatPanel — onboarding greeting auto-send (#449)", () => { expect(ChatPanel.consumePendingOnboardingGreeting()).toBe(true); expect(ChatPanel.consumePendingOnboardingGreeting()).toBe(false); }); + + it("the relay admits app-ready from the iframe (Lane 1 allowlist)", () => { + const cap = capturePanel(); + restore = cap.restore; + created = cap.created; + ChatPanel.openOrReveal(fakeCtx(), new URL("http://127.0.0.1:43117/")); + const html = cap.created[0].webview.html; + // app-ready must be in the Lane 1 allowlist (iframe → extension) + expect(html).toContain('"app-ready"'); + }); + + it("fires onAppReady callback when app-ready message arrives from iframe", async () => { + const cap = capturePanel(); + restore = cap.restore; + created = cap.created; + + const readyFired: boolean[] = []; + ChatPanel.onAppReady(() => readyFired.push(true)); + + const panel = ChatPanel.openOrReveal(fakeCtx(), new URL("http://127.0.0.1:43117/")); + + // Simulate the app-ready message arriving from the iframe + const webview = cap.created[0] as unknown as { webview: { _simulateMessage: (msg: unknown) => void } }; + webview.webview._simulateMessage({ source: "amicode", kind: "app-ready" }); + + await new Promise((r) => setTimeout(r, 10)); + expect(readyFired).toHaveLength(1); + }); + + it("postOnboardingGreeting falls back to posting after timeout if app-ready never fires", async () => { + const cap = capturePanel(); + restore = cap.restore; + created = cap.created; + + const panel = ChatPanel.openOrReveal(fakeCtx(), new URL("http://127.0.0.1:43117/")); + + const messages: unknown[] = []; + const webview = cap.created[0] as unknown as { webview: { postMessage: (m: unknown) => Promise; _simulateMessage: (msg: unknown) => void } }; + webview.webview.postMessage = (m: unknown) => { messages.push(m); return Promise.resolve(true); }; + + // Call with a short timeout for testing (pass timeout override) + panel.postOnboardingGreeting(200); + + // No app-ready — wait for the timeout fallback + await new Promise((r) => setTimeout(r, 300)); + + const navigateMsg = messages.find( + (m) => (m as { source?: string; kind?: string }).source === "amicode" && (m as { kind?: string }).kind === "navigate", + ); + expect(navigateMsg).toBeDefined(); + }); }); diff --git a/packages/extension/test/onboarding_panel.test.ts b/packages/extension/test/onboarding_panel.test.ts index 989f81d7..b6f36798 100644 --- a/packages/extension/test/onboarding_panel.test.ts +++ b/packages/extension/test/onboarding_panel.test.ts @@ -18,6 +18,7 @@ import { writeOnboardingConfig, testConnection, onOnboardingComplete, + dismissOnboardingPanel, _resetForTesting, } from "../src/onboarding_panel"; @@ -458,7 +459,7 @@ describe("Credential import — panel message handling (AC2, AC8, AC12, AC14)", spy.mockRestore(); }); - it("confirm-import writes batch config and disposes panel", async () => { + it("confirm-import keeps the panel alive as a transition splash (not disposed immediately)", async () => { const spy = vi.spyOn(vscode.window, "createWebviewPanel"); await vscode.commands.executeCommand("amicode.onboarding.open"); const panel = spy.mock.results[0].value as { @@ -482,14 +483,62 @@ describe("Credential import — panel message handling (AC2, AC8, AC12, AC14)", panel.webview._simulateMessage({ type: "scan-credentials" }); await new Promise((r) => setTimeout(r, 50)); - // Now confirm import (even if scan found nothing in test env, the handler should work) + // Now confirm import panel.webview._simulateMessage({ type: "confirm-import", payload: { activeProvider: "anthropic" }, }); await new Promise((r) => setTimeout(r, 50)); - // Panel should have been disposed (onboarding complete) + // Panel should NOT have been disposed yet — it's showing the transition splash + expect(disposeSpy).not.toHaveBeenCalled(); + + // Instead, the webview should have been told to show the transition state + const transitionMsg = postSpy.mock.calls + .map((c: unknown[]) => c[0]) + .find((m: { type: string }) => m.type === "show-transition"); + expect(transitionMsg).toBeDefined(); + + spy.mockRestore(); + }); + + it("dismissOnboardingPanel disposes the transition splash", async () => { + const spy = vi.spyOn(vscode.window, "createWebviewPanel"); + await vscode.commands.executeCommand("amicode.onboarding.open"); + const panel = spy.mock.results[0].value as { + webview: { + postMessage: ReturnType; + _simulateMessage: (msg: unknown) => void; + }; + dispose: ReturnType; + }; + + const disposeSpy = vi.fn(); + const origDispose = panel.dispose; + panel.dispose = (...args: unknown[]) => { + disposeSpy(); + return (origDispose as Function).apply(panel, args); + }; + + const postSpy = vi.fn().mockResolvedValue(true); + panel.webview.postMessage = postSpy; + + // Trigger scan + confirm to enter transition state + panel.webview._simulateMessage({ type: "scan-credentials" }); + await new Promise((r) => setTimeout(r, 50)); + panel.webview._simulateMessage({ + type: "confirm-import", + payload: { activeProvider: "anthropic" }, + }); + await new Promise((r) => setTimeout(r, 50)); + + // Panel still alive + expect(disposeSpy).not.toHaveBeenCalled(); + + // Now dismiss (extension calls this after app-ready) + dismissOnboardingPanel(); + + // Panel should now be disposed expect(disposeSpy).toHaveBeenCalled(); spy.mockRestore(); From 039ba07078db924f09a2c184fd8a54b6c66a55c7 Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Thu, 20 Aug 2026 23:05:01 +0200 Subject: [PATCH 19/43] fix(onboarding): transition splash actually visible MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three issues preventing the splash from showing: 1. animationEl was hidden (display:none, opacity:0) after the welcome animation completed — now explicitly restored on show-transition. 2. The onOnboardingComplete listener was opening ChatPanel immediately (racing the server restart and pushing the splash to background). Removed — chat now opens via the onReady path after restart. 3. The config-success handler (manual setup path) still had panel.dispose() instead of show-transition. Fixed to match confirm-import. --- packages/extension/src/extension.ts | 8 ++++---- packages/extension/src/onboarding_panel.ts | 3 ++- packages/extension/src/onboarding_webview.ts | 4 +++- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/extension/src/extension.ts b/packages/extension/src/extension.ts index a9c7c889..8418fee1 100644 --- a/packages/extension/src/extension.ts +++ b/packages/extension/src/extension.ts @@ -844,10 +844,10 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { // which then opens chat. if (!isModelConfigured() && vscode.workspace.getConfiguration("amicode").get("chat.autoOpen", true)) { void vscode.commands.executeCommand("amicode.onboarding.open"); - // Wire: when onboarding completes, auto-open chat - onOnboardingComplete(() => { - ChatPanel.openOrReveal(ctx, url, serverAuthToken(serverPassword), opencodeProject.projectDir); - }); + // Wire: when onboarding completes, the server restarts and the + // onReady handler (else-if branch below) opens the chat panel. + // We do NOT open chat here — that would race the server restart + // and show behind the transition splash. // Wire: when onboarding is cancelled (X), open chat normally onOnboardingCancelled(() => { ChatPanel.openOrReveal(ctx, url, serverAuthToken(serverPassword), opencodeProject.projectDir); diff --git a/packages/extension/src/onboarding_panel.ts b/packages/extension/src/onboarding_panel.ts index 64f6ab8b..48c00cae 100644 --- a/packages/extension/src/onboarding_panel.ts +++ b/packages/extension/src/onboarding_panel.ts @@ -374,7 +374,8 @@ export function registerOnboardingPanel(ctx: vscode.ExtensionContext): void { } else if (msg.type === "config-success") { const payload = msg.payload as OnboardingConfig; writeOnboardingConfig(payload); - panel.dispose(); + // Keep the panel alive as a transition splash (same as confirm-import) + void panel.webview.postMessage({ type: "show-transition" }); // Signal that the next chat panel open should auto-send the onboarding greeting ChatPanel.setPendingOnboardingGreeting(true); fireOnboardingComplete(); diff --git a/packages/extension/src/onboarding_webview.ts b/packages/extension/src/onboarding_webview.ts index b19e9306..c3b925b6 100644 --- a/packages/extension/src/onboarding_webview.ts +++ b/packages/extension/src/onboarding_webview.ts @@ -911,8 +911,10 @@ window.addEventListener("message", (event) => { // Hide the cancel button const cancelEl = document.getElementById("cancel-btn"); if (cancelEl) cancelEl.style.display = "none"; - // Show the animation container (it may already be visible if animation played) + // Show the animation container (restore from the post-animation hidden state) animationEl.style.display = "flex"; + animationEl.style.opacity = "1"; + animationEl.style.transition = "none"; // Add "Getting Amico ready..." text below the animation let transitionText = document.getElementById("transition-text"); if (!transitionText) { From 866752d1d6309d91bbadf598dfcde4ef2bacc844 Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Thu, 20 Aug 2026 23:06:42 +0200 Subject: [PATCH 20/43] fix: remove unused onOnboardingComplete import, fix Thenable return type --- packages/extension/src/chat_panel.ts | 2 +- packages/extension/src/extension.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/extension/src/chat_panel.ts b/packages/extension/src/chat_panel.ts index 7dc2e266..5973842d 100644 --- a/packages/extension/src/chat_panel.ts +++ b/packages/extension/src/chat_panel.ts @@ -157,7 +157,7 @@ export class ChatPanel { } /** Post an arbitrary message to the webview (relayed to the iframe). */ - postMessage(msg: unknown): Promise { + postMessage(msg: unknown): Thenable { return this.panel.webview.postMessage(msg); } diff --git a/packages/extension/src/extension.ts b/packages/extension/src/extension.ts index 8418fee1..fd0214c6 100644 --- a/packages/extension/src/extension.ts +++ b/packages/extension/src/extension.ts @@ -38,7 +38,7 @@ import { writeStopFile, savePulseTo, stopPlan, forceStop, runLogMtime } from "./ import { watchSolverMode, applyEntitlementForMode, readSolverModeState } from "./solver_mode"; import { runSetCloudKeyCommand } from "./cloud_key"; import { amicodeOpsDir } from "./substrate/vault_store"; -import { registerOnboardingPanel, onOnboardingComplete, onOnboardingCancelled, dismissOnboardingPanel } from "./onboarding_panel"; +import { registerOnboardingPanel, onOnboardingCancelled, dismissOnboardingPanel } from "./onboarding_panel"; import { isModelConfigured, writeWelcomeShown } from "./onboarding_routing"; import { stagePasqalConnector } from "./pasqal_assets"; import { needsProvision, pasqalVenvDir, provisionPasqalPython } from "./pasqal_python"; From e27387d4f094f4aff2ef103e4da598f64939cdc0 Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Thu, 20 Aug 2026 23:18:36 +0200 Subject: [PATCH 21/43] =?UTF-8?q?feat(onboarding):=20seamless=20panel=20ad?= =?UTF-8?q?option=20=E2=80=94=20splash=20fades=20into=20chat?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The onboarding panel transforms into the chat panel in-place via ChatPanel.adopt(). No second tab is created. The flow: 1. User confirms → onboarding webview shows splash (Amico + 'Getting ready') 2. Server restarts → onReady fires → extension adopts the onboarding panel 3. Panel HTML swaps to chat iframe with splash overlay (z-index on top) 4. App loads behind the overlay → posts app-ready 5. Overlay fades out (opacity + scale CSS transition, 400ms) 6. Chat is fully loaded underneath — onboarding session starts Key changes: - ChatPanel.adopt(panel, ctx, url, ...) — wraps existing panel as singleton - renderTransitionHtml() — iframe + splash overlay + relay script - getOnboardingPanel() / releaseOnboardingPanel() — panel handoff - Splash overlay CSS: fade-out class + scale(1.05) exit - Removes 'Get Started' button from splash (leftover from welcome anim) 93 tests pass. TypeScript clean. --- packages/extension/src/chat_panel.ts | 161 +++++++++++++++++- packages/extension/src/extension.ts | 27 +-- packages/extension/src/onboarding_panel.ts | 14 ++ packages/extension/src/onboarding_webview.ts | 3 + packages/extension/test/chat_panel.test.ts | 83 +++++++++ .../extension/test/onboarding_panel.test.ts | 16 ++ 6 files changed, 291 insertions(+), 13 deletions(-) diff --git a/packages/extension/src/chat_panel.ts b/packages/extension/src/chat_panel.ts index 5973842d..7bfbf63a 100644 --- a/packages/extension/src/chat_panel.ts +++ b/packages/extension/src/chat_panel.ts @@ -67,8 +67,11 @@ export class ChatPanel { opencodeUrl: URL, authToken?: string, hideProjectDir?: string, + withSplash?: boolean, ) { - this.panel.webview.html = this.renderHtml(opencodeUrl, authToken, hideProjectDir); + this.panel.webview.html = withSplash + ? this.renderTransitionHtml(opencodeUrl, authToken, hideProjectDir) + : this.renderHtml(opencodeUrl, authToken, hideProjectDir); ChatPanel.live.add(this); ChatPanel.onLiveChangeCallback?.(ChatPanel.live.size); this.panel.onDidDispose(() => this.dispose(), null, this.disposables); @@ -268,6 +271,29 @@ export class ChatPanel { return ChatPanel.current; } + /** Adopt an existing WebviewPanel (e.g. the onboarding panel) as the chat + * singleton. Swaps its HTML to the chat iframe with a splash overlay on top, + * wires message relay + bridge, and registers it as the primary ChatPanel. + * No new panel is created — zero tab switching. */ + static adopt( + panel: vscode.WebviewPanel, + ctx: vscode.ExtensionContext, + opencodeUrl: URL, + authToken?: string, + hideProjectDir?: string, + ): ChatPanel { + // If there's already a ChatPanel singleton, dispose it (shouldn't happen in normal flow) + if (ChatPanel.current) { + ChatPanel.current.dispose(); + } + const title = "Amicode Chat"; + const instance = new ChatPanel(panel, title, opencodeUrl, authToken, hideProjectDir, true); + ChatPanel.current = instance; + panel.title = title; + panel.iconPath = tabIconPath(ctx); + return instance; + } + /** Side-by-side sessions: ALWAYS a fresh tab beside the active editor — the * caller pins the tab's session scope via the URL (e.g. the app's * /new-session draft route), so each tab owns its conversation while sharing @@ -405,6 +431,139 @@ export class ChatPanel { `; } + /** Render the chat iframe HTML with a splash overlay on top. + * Used by adopt() — the overlay fades out when app-ready fires, revealing + * the fully-loaded chat underneath. Zero tab switching, pure CSS transition. */ + private renderTransitionHtml(opencodeUrl: URL, authToken?: string, hideProjectDir?: string): string { + const nonce = randomBytes(16).toString("base64"); + const csp = [ + "default-src 'none'", + "style-src 'unsafe-inline'", + `script-src 'nonce-${nonce}'`, + `frame-src ${opencodeUrl.origin}`, + "connect-src 'self'", + ].join("; "); + const origin = JSON.stringify(opencodeUrl.origin); + const framed = new URL(opencodeUrl.href); + framed.searchParams.set("colorScheme", themeKindToScheme(vscode.window.activeColorTheme.kind)); + if (authToken) framed.searchParams.set("auth_token", authToken); + if (hideProjectDir) framed.searchParams.set("amicode_hide_project", hideProjectDir); + if (ChatPanel.bugReportAvailable) framed.searchParams.set("amicode_bug_report", "1"); + return /* html */ ` + + + + + + + +
+ + + + + + +
Getting Amico ready...
+
+ + + +`; + } + dispose(): void { for (const d of this.disposables) { try { diff --git a/packages/extension/src/extension.ts b/packages/extension/src/extension.ts index fd0214c6..25ee3221 100644 --- a/packages/extension/src/extension.ts +++ b/packages/extension/src/extension.ts @@ -38,7 +38,7 @@ import { writeStopFile, savePulseTo, stopPlan, forceStop, runLogMtime } from "./ import { watchSolverMode, applyEntitlementForMode, readSolverModeState } from "./solver_mode"; import { runSetCloudKeyCommand } from "./cloud_key"; import { amicodeOpsDir } from "./substrate/vault_store"; -import { registerOnboardingPanel, onOnboardingCancelled, dismissOnboardingPanel } from "./onboarding_panel"; +import { registerOnboardingPanel, onOnboardingCancelled, getOnboardingPanel, releaseOnboardingPanel } from "./onboarding_panel"; import { isModelConfigured, writeWelcomeShown } from "./onboarding_routing"; import { stagePasqalConnector } from "./pasqal_assets"; import { needsProvision, pasqalVenvDir, provisionPasqalPython } from "./pasqal_python"; @@ -854,18 +854,21 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { }); } else if (vscode.workspace.getConfiguration("amicode").get("chat.autoOpen", true)) { // Normal path: model configured → open chat directly - const panel = ChatPanel.openOrReveal(ctx, url, serverAuthToken(serverPassword), opencodeProject.projectDir); - // Post-onboarding: send navigate message to the existing panel to - // start a new session with "Begin onboarding" auto-sent. The navigate - // message fires event-driven (on app-ready), not on a blind timer. - // Also dismiss the onboarding splash panel once the app is ready. + // Post-onboarding: adopt the onboarding panel as the chat panel (zero + // tab switching — the splash overlay fades out revealing the chat). if (ChatPanel.consumePendingOnboardingGreeting()) { - let dismissed = false; - const dismiss = () => { if (!dismissed) { dismissed = true; dismissOnboardingPanel(); } }; - ChatPanel.onAppReady(dismiss); - // Safety: force-dismiss after 10s if app-ready never fires. - setTimeout(dismiss, 10_000); - panel.postOnboardingGreeting(); + const onboardPanel = getOnboardingPanel(); + if (onboardPanel) { + releaseOnboardingPanel(); // detach from onboarding lifecycle + const panel = ChatPanel.adopt(onboardPanel, ctx, url, serverAuthToken(serverPassword), opencodeProject.projectDir); + panel.postOnboardingGreeting(); + } else { + // Fallback: no onboarding panel alive (user closed it manually) + const panel = ChatPanel.openOrReveal(ctx, url, serverAuthToken(serverPassword), opencodeProject.projectDir); + panel.postOnboardingGreeting(); + } + } else { + ChatPanel.openOrReveal(ctx, url, serverAuthToken(serverPassword), opencodeProject.projectDir); } } // Surface ONE explicit LLM-provider signal at boot, read from opencode's diff --git a/packages/extension/src/onboarding_panel.ts b/packages/extension/src/onboarding_panel.ts index 48c00cae..d456b2fc 100644 --- a/packages/extension/src/onboarding_panel.ts +++ b/packages/extension/src/onboarding_panel.ts @@ -337,6 +337,20 @@ export function dismissOnboardingPanel(): void { } } +/** Return the live onboarding WebviewPanel (if one exists). Used by the + * transition flow: the extension swaps its HTML and adopts it as the chat + * panel — zero tab switching. */ +export function getOnboardingPanel(): vscode.WebviewPanel | undefined { + return currentPanel; +} + +/** Detach the onboarding panel from this module's lifecycle tracking WITHOUT + * disposing it. Called when ChatPanel.adopt() takes ownership. After this, + * dismissOnboardingPanel() is a no-op and re-opening creates a fresh panel. */ +export function releaseOnboardingPanel(): void { + currentPanel = undefined; +} + /** Register the onboarding panel command. Call from extension.ts activate(). */ export function registerOnboardingPanel(ctx: vscode.ExtensionContext): void { ctx.subscriptions.push( diff --git a/packages/extension/src/onboarding_webview.ts b/packages/extension/src/onboarding_webview.ts index c3b925b6..18964dac 100644 --- a/packages/extension/src/onboarding_webview.ts +++ b/packages/extension/src/onboarding_webview.ts @@ -911,6 +911,9 @@ window.addEventListener("message", (event) => { // Hide the cancel button const cancelEl = document.getElementById("cancel-btn"); if (cancelEl) cancelEl.style.display = "none"; + // Remove the "Get Started" button left over from the welcome animation + const ctaBtn = animationEl.querySelector(".welcome-cta"); + if (ctaBtn) ctaBtn.remove(); // Show the animation container (restore from the post-animation hidden state) animationEl.style.display = "flex"; animationEl.style.opacity = "1"; diff --git a/packages/extension/test/chat_panel.test.ts b/packages/extension/test/chat_panel.test.ts index 4eaa92f2..cf9fb579 100644 --- a/packages/extension/test/chat_panel.test.ts +++ b/packages/extension/test/chat_panel.test.ts @@ -245,3 +245,86 @@ describe("ChatPanel — onboarding greeting auto-send (#449)", () => { expect(navigateMsg).toBeDefined(); }); }); + +describe("ChatPanel.adopt — transforms an existing panel into the chat singleton", () => { + let restore: (() => void) | undefined; + let created: CapturedPanel[] = []; + afterEach(() => { + for (const p of created) p.dispose(); + restore?.(); + restore = undefined; + created = []; + ChatPanel.clearPendingOnboardingGreeting(); + ChatPanel.clearAppReadyCallbacks(); + }); + + it("adopt wraps an existing WebviewPanel as the ChatPanel singleton (no new panel created)", () => { + // Create a panel externally BEFORE installing the capture spy + const existingPanel = vscode.window.createWebviewPanel( + "amicode.onboarding", "Amicode Setup", vscode.ViewColumn.One, { enableScripts: true }, + ) as unknown as CapturedPanel; + created.push(existingPanel); + + // Now install the spy — any new panel creation will be captured + const cap = capturePanel(); + restore = cap.restore; + + // Adopt it + const chatPanel = ChatPanel.adopt( + existingPanel as unknown as import("vscode").WebviewPanel, + fakeCtx(), + new URL("http://127.0.0.1:43117/"), + ); + + expect(chatPanel).toBeDefined(); + // No NEW panel should have been created via createWebviewPanel + expect(cap.created).toHaveLength(0); + // openOrReveal should now return the adopted panel (it's the singleton) + const revealed = ChatPanel.openOrReveal(fakeCtx(), new URL("http://127.0.0.1:43117/")); + expect(revealed).toBe(chatPanel); + // Still no new panel + expect(cap.created).toHaveLength(0); + }); + + it("adopt sets the panel HTML to the chat iframe content with splash overlay", () => { + const existingPanel = vscode.window.createWebviewPanel( + "amicode.onboarding", "Amicode Setup", vscode.ViewColumn.One, { enableScripts: true }, + ) as unknown as CapturedPanel; + created.push(existingPanel); + + ChatPanel.adopt( + existingPanel as unknown as import("vscode").WebviewPanel, + fakeCtx(), + new URL("http://127.0.0.1:43117/"), + ); + + // The HTML should contain both the iframe and the splash overlay + const html = existingPanel.webview.html; + expect(html).toContain("iframe"); + expect(html).toContain("splash-overlay"); + expect(html).toContain("127.0.0.1:43117"); + }); + + it("adopt wires app-ready so it fires onAppReady callbacks", async () => { + const existingPanel = vscode.window.createWebviewPanel( + "amicode.onboarding", "Amicode Setup", vscode.ViewColumn.One, { enableScripts: true }, + ) as unknown as CapturedPanel; + created.push(existingPanel); + + const readyFired: boolean[] = []; + ChatPanel.onAppReady(() => readyFired.push(true)); + + ChatPanel.adopt( + existingPanel as unknown as import("vscode").WebviewPanel, + fakeCtx(), + new URL("http://127.0.0.1:43117/"), + ); + + // Simulate app-ready arriving from the iframe + const webview = existingPanel as unknown as { webview: { _simulateMessage: (msg: unknown) => void } }; + webview.webview._simulateMessage({ source: "amicode", kind: "app-ready" }); + + await new Promise((r) => setTimeout(r, 10)); + expect(readyFired).toHaveLength(1); + }); +}); diff --git a/packages/extension/test/onboarding_panel.test.ts b/packages/extension/test/onboarding_panel.test.ts index b6f36798..27a58532 100644 --- a/packages/extension/test/onboarding_panel.test.ts +++ b/packages/extension/test/onboarding_panel.test.ts @@ -19,6 +19,8 @@ import { testConnection, onOnboardingComplete, dismissOnboardingPanel, + getOnboardingPanel, + releaseOnboardingPanel, _resetForTesting, } from "../src/onboarding_panel"; @@ -64,6 +66,20 @@ describe("OnboardingPanel — panel lifecycle (AC1, AC6, AC7)", () => { spy.mockRestore(); }); + it("getOnboardingPanel returns the live panel, releaseOnboardingPanel detaches it", async () => { + expect(getOnboardingPanel()).toBeUndefined(); // no panel yet + const spy = vi.spyOn(vscode.window, "createWebviewPanel"); + await vscode.commands.executeCommand("amicode.onboarding.open"); + const panel = getOnboardingPanel(); + expect(panel).toBeDefined(); + // Release detaches without disposing + releaseOnboardingPanel(); + expect(getOnboardingPanel()).toBeUndefined(); + // Panel is still alive (not disposed) + expect((panel as any).webview).toBeDefined(); + spy.mockRestore(); + }); + it("AC7: fires an event after onboarding completes", async () => { const fired: boolean[] = []; const disposable = onOnboardingComplete(() => { From db46761ebcd2030b262cef2bcda8f1c13018709d Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Thu, 20 Aug 2026 23:21:07 +0200 Subject: [PATCH 22/43] fix(onboarding): splash displays for minimum 10 seconds Even if the app loads fast, the 'Getting Amico ready...' splash holds for at least 10s before fading. The app-ready relay to the extension fires immediately (so navigate posts on time), but the visual fade waits for the remaining duration. --- packages/extension/src/chat_panel.ts | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/packages/extension/src/chat_panel.ts b/packages/extension/src/chat_panel.ts index 7bfbf63a..43a6ea08 100644 --- a/packages/extension/src/chat_panel.ts +++ b/packages/extension/src/chat_panel.ts @@ -499,16 +499,26 @@ export class ChatPanel { (function () { var vscode = acquireVsCodeApi(); var origin = ${origin}; + var splashStart = Date.now(); + var MIN_SPLASH_MS = 10000; // minimum 10s display time + + function fadeSplash() { + var splash = document.getElementById("splash"); + if (splash) { + splash.classList.add("fade-out"); + splash.addEventListener("transitionend", function () { splash.remove(); }); + } + } + window.addEventListener("message", function (e) { var d = e.data; if (e.origin === origin) { - // app-ready: fade the splash overlay and relay to extension + // app-ready: fade the splash overlay after minimum display time if (d && d.source === "amicode" && d.kind === "app-ready") { - var splash = document.getElementById("splash"); - if (splash) { - splash.classList.add("fade-out"); - splash.addEventListener("transitionend", function () { splash.remove(); }); - } + var elapsed = Date.now() - splashStart; + var remaining = Math.max(0, MIN_SPLASH_MS - elapsed); + setTimeout(fadeSplash, remaining); + // Relay to extension immediately (so navigate posts on time) vscode.postMessage(d); return; } From 7283100462c9992593f5a867ef9b58ee9f51dd06 Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Thu, 20 Aug 2026 23:24:58 +0200 Subject: [PATCH 23/43] fix(onboarding): use real Amico mark in transition splash, reduce min to 5s - Replaced placeholder rectangles with the actual detailed Amico SVG (bracket + eyes + carets) with a breathing + blink animation - Reduced minimum splash display from 10s to 5s --- packages/extension/src/chat_panel.ts | 62 +++++++++++++++++++++++----- 1 file changed, 52 insertions(+), 10 deletions(-) diff --git a/packages/extension/src/chat_panel.ts b/packages/extension/src/chat_panel.ts index 43a6ea08..46001938 100644 --- a/packages/extension/src/chat_panel.ts +++ b/packages/extension/src/chat_panel.ts @@ -474,23 +474,65 @@ export class ChatPanel { font-family: var(--vscode-font-family, system-ui); } .splash-mark { - width: 80px; height: 80px; + width: 120px; height: 107px; fill: var(--vscode-foreground, #ccc); - animation: breathe 3s ease-in-out infinite; + overflow: visible; + } + .splash-mark .mark-breathe { + transform-box: fill-box; transform-origin: 50% 100%; + animation: breathe 4.5s ease-in-out infinite; + } + .splash-mark .eye-ring { + transform-box: fill-box; transform-origin: center; + animation: blink 7s linear infinite; + } + .splash-mark .eye-lid { + transform-box: fill-box; transform-origin: center; + opacity: 0; } @keyframes breathe { - 0%, 100% { transform: scale(1); } - 50% { transform: scale(1.04); } + 0%, 100% { transform: scale(1, 1); } + 50% { transform: scale(0.992, 1.014); } + } + @keyframes blink { + 0%, 30% { transform: scaleY(1); } + 31.3% { transform: scaleY(0.1); } + 32.2% { transform: scaleY(0.1); } + 35.2%, 100% { transform: scaleY(1); } }
- - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Getting Amico ready...
@@ -500,7 +542,7 @@ export class ChatPanel { var vscode = acquireVsCodeApi(); var origin = ${origin}; var splashStart = Date.now(); - var MIN_SPLASH_MS = 10000; // minimum 10s display time + var MIN_SPLASH_MS = 5000; // minimum 5s display time function fadeSplash() { var splash = document.getElementById("splash"); From 804d622bbe231a4f9ce198d6c482ee5a14c79680 Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Thu, 20 Aug 2026 23:28:31 +0200 Subject: [PATCH 24/43] fix(onboarding): yellow robot with excited jump, instant text - Splash mark uses brand accent (lemon #fff676 on dark, foreground on light) matching the onboarding welcome animation exactly - Replaced breathing with an excited jump animation (squash + bounce) - 'Getting Amico ready...' appears instantly (no fade-in animation) --- packages/extension/src/chat_panel.ts | 18 +++++++++++++----- packages/extension/src/onboarding_webview.ts | 1 - 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/packages/extension/src/chat_panel.ts b/packages/extension/src/chat_panel.ts index 46001938..32f73e52 100644 --- a/packages/extension/src/chat_panel.ts +++ b/packages/extension/src/chat_panel.ts @@ -475,12 +475,16 @@ export class ChatPanel { } .splash-mark { width: 120px; height: 107px; - fill: var(--vscode-foreground, #ccc); + fill: var(--color-accent-ink, #fff676); overflow: visible; } + body.vscode-light .splash-mark, + body.vscode-high-contrast-light .splash-mark { + fill: var(--color-accent-ink, var(--vscode-foreground, #424242)); + } .splash-mark .mark-breathe { transform-box: fill-box; transform-origin: 50% 100%; - animation: breathe 4.5s ease-in-out infinite; + animation: jump 2.0s ease-in-out infinite; } .splash-mark .eye-ring { transform-box: fill-box; transform-origin: center; @@ -490,9 +494,13 @@ export class ChatPanel { transform-box: fill-box; transform-origin: center; opacity: 0; } - @keyframes breathe { - 0%, 100% { transform: scale(1, 1); } - 50% { transform: scale(0.992, 1.014); } + @keyframes jump { + 0%, 40% { transform: translateY(0) scale(1, 1); } + 46% { transform: translateY(0) scale(1.08, 0.92); } + 58% { transform: translateY(-60px) scale(0.96, 1.05); } + 70% { transform: translateY(0) scale(1.06, 0.94); } + 80% { transform: translateY(-20px) scale(0.99, 1.02); } + 88%, 100% { transform: translateY(0) scale(1, 1); } } @keyframes blink { 0%, 30% { transform: scaleY(1); } diff --git a/packages/extension/src/onboarding_webview.ts b/packages/extension/src/onboarding_webview.ts index 18964dac..5af9d2ff 100644 --- a/packages/extension/src/onboarding_webview.ts +++ b/packages/extension/src/onboarding_webview.ts @@ -926,7 +926,6 @@ window.addEventListener("message", (event) => { transitionText.style.cssText = ` text-align: center; margin-top: 24px; font-size: 14px; color: var(--vscode-descriptionForeground, #999); - animation: fadeIn 0.4s ease-out; `; transitionText.textContent = "Getting Amico ready..."; animationEl.parentElement!.insertBefore(transitionText, animationEl.nextSibling); From e601f410a0d2b5266d408d5600c047d9b29b7c24 Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Thu, 20 Aug 2026 23:33:22 +0200 Subject: [PATCH 25/43] fix(onboarding): happy grinning Amico with excited jump MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Eyes are now upside-down U shapes (∩) expressing glee - Added a wide grin below the nose divider - Both the onboarding webview transition AND the adopt splash use the same happy expression (no flash between them) - Onboarding webview dynamically swaps the square eyes for happy arcs and adds the grin when show-transition fires - Removed blink animation (closed happy eyes don't blink) - Text appears instantly --- packages/extension/src/chat_panel.ts | 44 +++++--------------- packages/extension/src/onboarding_webview.ts | 37 ++++++++++++++++ 2 files changed, 48 insertions(+), 33 deletions(-) diff --git a/packages/extension/src/chat_panel.ts b/packages/extension/src/chat_panel.ts index 32f73e52..4b8d31ec 100644 --- a/packages/extension/src/chat_panel.ts +++ b/packages/extension/src/chat_panel.ts @@ -474,7 +474,7 @@ export class ChatPanel { font-family: var(--vscode-font-family, system-ui); } .splash-mark { - width: 120px; height: 107px; + width: 176px; height: 157px; fill: var(--color-accent-ink, #fff676); overflow: visible; } @@ -486,14 +486,6 @@ export class ChatPanel { transform-box: fill-box; transform-origin: 50% 100%; animation: jump 2.0s ease-in-out infinite; } - .splash-mark .eye-ring { - transform-box: fill-box; transform-origin: center; - animation: blink 7s linear infinite; - } - .splash-mark .eye-lid { - transform-box: fill-box; transform-origin: center; - opacity: 0; - } @keyframes jump { 0%, 40% { transform: translateY(0) scale(1, 1); } 46% { transform: translateY(0) scale(1.08, 0.92); } @@ -502,41 +494,27 @@ export class ChatPanel { 80% { transform: translateY(-20px) scale(0.99, 1.02); } 88%, 100% { transform: translateY(0) scale(1, 1); } } - @keyframes blink { - 0%, 30% { transform: scaleY(1); } - 31.3% { transform: scaleY(0.1); } - 32.2% { transform: scaleY(0.1); } - 35.2%, 100% { transform: scaleY(1); } - }
+ + - - - - - - - - - + + + - - - - - - - - - + + + + + diff --git a/packages/extension/src/onboarding_webview.ts b/packages/extension/src/onboarding_webview.ts index 5af9d2ff..d8f51566 100644 --- a/packages/extension/src/onboarding_webview.ts +++ b/packages/extension/src/onboarding_webview.ts @@ -914,10 +914,47 @@ window.addEventListener("message", (event) => { // Remove the "Get Started" button left over from the welcome animation const ctaBtn = animationEl.querySelector(".welcome-cta"); if (ctaBtn) ctaBtn.remove(); + // Remove the welcome text ("Welcome" / subtitle) + const welcomeText = animationEl.querySelector(".welcome-text"); + if (welcomeText) welcomeText.remove(); // Show the animation container (restore from the post-animation hidden state) animationEl.style.display = "flex"; animationEl.style.opacity = "1"; animationEl.style.transition = "none"; + + // Swap the face to happy expression: replace square eyes with ∩ arcs + add grin + const svg = animationEl.querySelector(".amico-mark"); + if (svg) { + // Remove existing eyes + const leftEye = svg.querySelector(".left-eye"); + const rightEye = svg.querySelector(".right-eye"); + const divider = svg.querySelector(".divider"); + if (leftEye) leftEye.remove(); + if (rightEye) rightEye.remove(); + + // Find the inner-most animated group to append to + const enterGroup = svg.querySelector(".mark-enter") || svg.querySelector(".mark-breathe"); + if (enterGroup) { + // Add happy eyes (∩ shapes) and grin + const ns = "http://www.w3.org/2000/svg"; + const leftHappy = document.createElementNS(ns, "path"); + leftHappy.setAttribute("d", "M1160,1750 C1160,1350 1620,1350 1620,1750 L1490,1750 C1490,1500 1290,1500 1290,1750 Z"); + const rightHappy = document.createElementNS(ns, "path"); + rightHappy.setAttribute("d", "M2030,1750 C2030,1350 2490,1350 2490,1750 L2360,1750 C2360,1500 2160,1500 2160,1750 Z"); + const grin = document.createElementNS(ns, "path"); + grin.setAttribute("d", "M1350,2100 C1500,2380 2100,2380 2250,2100 L2130,2100 C2020,2280 1580,2280 1470,2100 Z"); + enterGroup.appendChild(leftHappy); + enterGroup.appendChild(rightHappy); + enterGroup.appendChild(grin); + } + + // Switch from idle animations to excited jump + const breatheGroup = svg.querySelector(".mark-breathe") as HTMLElement; + if (breatheGroup) { + breatheGroup.style.animation = "amico-jump 2.0s ease-in-out infinite"; + } + } + // Add "Getting Amico ready..." text below the animation let transitionText = document.getElementById("transition-text"); if (!transitionText) { From ac148cae7bd7e120cb9151ec755d743c6e202a04 Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Thu, 20 Aug 2026 23:36:45 +0200 Subject: [PATCH 26/43] fix(onboarding): pixelated happy face, constant-size opening, gentle fade MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Opening screen: - Robot and 'Welcome to Amicode' appear at constant size (fade only, no drop/bounce/scale entrance animation) - 'Get Started' button fades in gently (1s ease-in) underneath Transition splash: - Eyes are pixelated ∩ shapes (original eye rects minus bottom bar) - Mouth is a pixelated open U-grin (3 rectangles) - Both onboarding webview and adopt HTML use the same pixel-art style - Minimum 5s splash display time --- packages/extension/src/chat_panel.ts | 18 ++-- packages/extension/src/onboarding_webview.ts | 88 ++++++++++---------- 2 files changed, 57 insertions(+), 49 deletions(-) diff --git a/packages/extension/src/chat_panel.ts b/packages/extension/src/chat_panel.ts index 4b8d31ec..76039284 100644 --- a/packages/extension/src/chat_panel.ts +++ b/packages/extension/src/chat_panel.ts @@ -506,14 +506,20 @@ export class ChatPanel { - - + + + + - - - - + + + + + + + + diff --git a/packages/extension/src/onboarding_webview.ts b/packages/extension/src/onboarding_webview.ts index d8f51566..61b3c434 100644 --- a/packages/extension/src/onboarding_webview.ts +++ b/packages/extension/src/onboarding_webview.ts @@ -96,31 +96,10 @@ function playWelcomeAnimation(): void { entrance and snap open at 0.90s. */ .amico-mark .eye-lid { opacity: 0; } - /* 0.00s — Amico fades in and drops onto his feet, bouncing twice before - he settles. Volume is roughly conserved: he widens as he flattens. - Per-keyframe easing does the real work — falls accelerate, rises - decelerate; a single curve across the whole thing reads as floaty. */ + /* 0.00s — Amico fades in at constant size. No drop, no bounce, + no scale — just appears. The button fades in after. */ @keyframes amico-enter { - 0% { - opacity: 0; transform: translateY(-260px) scale(0.92, 1.10); - animation-timing-function: cubic-bezier(0.4, 0, 1, 1); - } - 25% { - opacity: 1; transform: translateY(0) scale(1.14, 0.86); - animation-timing-function: cubic-bezier(0, 0, 0.3, 1); - } - 45% { - transform: translateY(-190px) scale(0.96, 1.06); - animation-timing-function: cubic-bezier(0.4, 0, 1, 1); - } - 65% { - transform: translateY(0) scale(1.08, 0.93); - animation-timing-function: cubic-bezier(0, 0, 0.3, 1); - } - 82% { - transform: translateY(-70px) scale(0.99, 1.02); - animation-timing-function: cubic-bezier(0.4, 0, 1, 1); - } + 0% { opacity: 0; } 100% { opacity: 1; transform: translateY(0) scale(1, 1); } } @@ -237,8 +216,8 @@ function playWelcomeAnimation(): void { } @keyframes amico-rise { - from { opacity: 0; transform: translateY(8px); } - to { opacity: 1; transform: translateY(0); } + from { opacity: 0; } + to { opacity: 1; } } @keyframes amico-fade-in { from { opacity: 0; } @@ -349,7 +328,7 @@ function playWelcomeAnimation(): void { color: var(--color-on-accent, #000); border: var(--border-width, 1px) solid var(--color-on-accent, #000); border-radius: var(--border-radius, 4px); - cursor: pointer; opacity: 0; transition: opacity 0.5s ease-in, filter 0.16s ease; + cursor: pointer; opacity: 0; transition: opacity 1s ease-in, filter 0.16s ease; `; logo.appendChild(btn); requestAnimationFrame(() => { btn.style.opacity = "1"; }); @@ -922,30 +901,53 @@ window.addEventListener("message", (event) => { animationEl.style.opacity = "1"; animationEl.style.transition = "none"; - // Swap the face to happy expression: replace square eyes with ∩ arcs + add grin + // Swap the face to happy expression: remove bottom eye bars + add pixelated open grin const svg = animationEl.querySelector(".amico-mark"); if (svg) { - // Remove existing eyes + // Remove the bottom bar from each eye (makes ∩ shape = happy closed eyes) const leftEye = svg.querySelector(".left-eye"); const rightEye = svg.querySelector(".right-eye"); - const divider = svg.querySelector(".divider"); - if (leftEye) leftEye.remove(); - if (rightEye) rightEye.remove(); + if (leftEye) { + // The 4th rect in eye-ring is the bottom bar (y ≈ 1870) + const rects = leftEye.querySelectorAll(".eye-ring rect"); + if (rects.length >= 4) rects[3].remove(); + // Remove the lid too (not needed for happy eyes) + const lid = leftEye.querySelector(".eye-lid"); + if (lid) lid.remove(); + } + if (rightEye) { + const rects = rightEye.querySelectorAll(".eye-ring rect"); + if (rects.length >= 4) rects[3].remove(); + const lid = rightEye.querySelector(".eye-lid"); + if (lid) lid.remove(); + } + // Stop eye animations (happy eyes don't blink) + if (leftEye) { + const ring = leftEye.querySelector(".eye-ring") as HTMLElement; + if (ring) ring.style.animation = "none"; + } + if (rightEye) { + const ring = rightEye.querySelector(".eye-ring") as HTMLElement; + if (ring) ring.style.animation = "none"; + } - // Find the inner-most animated group to append to + // Find the inner-most animated group to append the mouth const enterGroup = svg.querySelector(".mark-enter") || svg.querySelector(".mark-breathe"); if (enterGroup) { - // Add happy eyes (∩ shapes) and grin + // Add pixelated U-shaped open mouth (gleeful grin) const ns = "http://www.w3.org/2000/svg"; - const leftHappy = document.createElementNS(ns, "path"); - leftHappy.setAttribute("d", "M1160,1750 C1160,1350 1620,1350 1620,1750 L1490,1750 C1490,1500 1290,1500 1290,1750 Z"); - const rightHappy = document.createElementNS(ns, "path"); - rightHappy.setAttribute("d", "M2030,1750 C2030,1350 2490,1350 2490,1750 L2360,1750 C2360,1500 2160,1500 2160,1750 Z"); - const grin = document.createElementNS(ns, "path"); - grin.setAttribute("d", "M1350,2100 C1500,2380 2100,2380 2250,2100 L2130,2100 C2020,2280 1580,2280 1470,2100 Z"); - enterGroup.appendChild(leftHappy); - enterGroup.appendChild(rightHappy); - enterGroup.appendChild(grin); + const mouthL = document.createElementNS(ns, "rect"); + mouthL.setAttribute("x", "1430"); mouthL.setAttribute("y", "2080"); + mouthL.setAttribute("width", "133"); mouthL.setAttribute("height", "280"); + const mouthR = document.createElementNS(ns, "rect"); + mouthR.setAttribute("x", "2090"); mouthR.setAttribute("y", "2080"); + mouthR.setAttribute("width", "133"); mouthR.setAttribute("height", "280"); + const mouthBottom = document.createElementNS(ns, "rect"); + mouthBottom.setAttribute("x", "1563"); mouthBottom.setAttribute("y", "2230"); + mouthBottom.setAttribute("width", "527"); mouthBottom.setAttribute("height", "130"); + enterGroup.appendChild(mouthL); + enterGroup.appendChild(mouthR); + enterGroup.appendChild(mouthBottom); } // Switch from idle animations to excited jump From d7b77df694e902e2f6b8b945e0080605ff201921 Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Thu, 20 Aug 2026 23:40:13 +0200 Subject: [PATCH 27/43] =?UTF-8?q?fix(onboarding):=20shorter=20=E2=88=A9=20?= =?UTF-8?q?eyes,=20button=20no=20longer=20nudges=20layout?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Shaved one pixel height off the happy ∩ eye side bars (423→286 units) for a more squinted/gleeful look - 'Get Started' button is now pre-allocated in the DOM (visibility:hidden, opacity:0) so it doesn't shift the robot + text when it fades in - Button fades in smoothly without any layout reflow --- packages/extension/src/chat_panel.ts | 12 +++++----- packages/extension/src/onboarding_webview.ts | 24 +++++++++----------- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/packages/extension/src/chat_panel.ts b/packages/extension/src/chat_panel.ts index 76039284..fbfbb126 100644 --- a/packages/extension/src/chat_panel.ts +++ b/packages/extension/src/chat_panel.ts @@ -506,15 +506,15 @@ export class ChatPanel { - - - + + + - - - + + + diff --git a/packages/extension/src/onboarding_webview.ts b/packages/extension/src/onboarding_webview.ts index 61b3c434..4335f59c 100644 --- a/packages/extension/src/onboarding_webview.ts +++ b/packages/extension/src/onboarding_webview.ts @@ -306,6 +306,14 @@ function playWelcomeAnimation(): void {

Welcome to Amicode

+
`; @@ -318,19 +326,9 @@ function playWelcomeAnimation(): void { // Show "Get Started" button after text fades in, user clicks to proceed setTimeout(() => { - const btn = document.createElement("button"); - btn.textContent = "Get Started"; - btn.className = "welcome-cta"; - btn.style.cssText = ` - margin-top: 40px; padding: 10px 32px; - font-family: var(--text-font, inherit); font-size: 14px; font-weight: 500; - background: var(--color-accent-fill, #fff676); - color: var(--color-on-accent, #000); - border: var(--border-width, 1px) solid var(--color-on-accent, #000); - border-radius: var(--border-radius, 4px); - cursor: pointer; opacity: 0; transition: opacity 1s ease-in, filter 0.16s ease; - `; - logo.appendChild(btn); + const btn = logo.querySelector(".welcome-cta") as HTMLButtonElement; + if (!btn) return; + btn.style.visibility = "visible"; requestAnimationFrame(() => { btn.style.opacity = "1"; }); btn.addEventListener("click", () => { From ebf0268c9fda35482395507a1ab37491d1f3dce7 Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Thu, 20 Aug 2026 23:43:37 +0200 Subject: [PATCH 28/43] fix(onboarding): 3s button delay with 2s fade, wider natural grin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 'Get Started' waits 3 seconds before fading in (2s ease-in transition) - Grin is now a single wide bar (793 units) — no corner pixels, cleaner and more natural as a beaming smile - Both transition HTML and onboarding webview use the same grin style --- packages/extension/src/chat_panel.ts | 6 ++---- packages/extension/src/onboarding_webview.ts | 22 +++++++------------- 2 files changed, 9 insertions(+), 19 deletions(-) diff --git a/packages/extension/src/chat_panel.ts b/packages/extension/src/chat_panel.ts index fbfbb126..d88ede70 100644 --- a/packages/extension/src/chat_panel.ts +++ b/packages/extension/src/chat_panel.ts @@ -516,10 +516,8 @@ export class ChatPanel { - - - - + + diff --git a/packages/extension/src/onboarding_webview.ts b/packages/extension/src/onboarding_webview.ts index 4335f59c..e8785dea 100644 --- a/packages/extension/src/onboarding_webview.ts +++ b/packages/extension/src/onboarding_webview.ts @@ -311,7 +311,7 @@ function playWelcomeAnimation(): void { background: var(--color-accent-fill, #fff676); color: var(--color-on-accent, #000); border: var(--border-width, 1px) solid var(--color-on-accent, #000); border-radius: var(--border-radius, 4px); - cursor: pointer; opacity: 0; visibility: hidden; transition: opacity 1s ease-in, filter 0.16s ease;"> + cursor: pointer; opacity: 0; visibility: hidden; transition: opacity 2s ease-in, filter 0.16s ease;"> Get Started
@@ -339,7 +339,7 @@ function playWelcomeAnimation(): void { revealForm(); }, 400); }); - }, 2000); + }, 3000); } // ─── Form ──────────────────────────────────────────────────────────────────── @@ -932,20 +932,12 @@ window.addEventListener("message", (event) => { // Find the inner-most animated group to append the mouth const enterGroup = svg.querySelector(".mark-enter") || svg.querySelector(".mark-breathe"); if (enterGroup) { - // Add pixelated U-shaped open mouth (gleeful grin) + // Add wide grin (single bar, no corners) const ns = "http://www.w3.org/2000/svg"; - const mouthL = document.createElementNS(ns, "rect"); - mouthL.setAttribute("x", "1430"); mouthL.setAttribute("y", "2080"); - mouthL.setAttribute("width", "133"); mouthL.setAttribute("height", "280"); - const mouthR = document.createElementNS(ns, "rect"); - mouthR.setAttribute("x", "2090"); mouthR.setAttribute("y", "2080"); - mouthR.setAttribute("width", "133"); mouthR.setAttribute("height", "280"); - const mouthBottom = document.createElementNS(ns, "rect"); - mouthBottom.setAttribute("x", "1563"); mouthBottom.setAttribute("y", "2230"); - mouthBottom.setAttribute("width", "527"); mouthBottom.setAttribute("height", "130"); - enterGroup.appendChild(mouthL); - enterGroup.appendChild(mouthR); - enterGroup.appendChild(mouthBottom); + const grin = document.createElementNS(ns, "rect"); + grin.setAttribute("x", "1430"); grin.setAttribute("y", "2150"); + grin.setAttribute("width", "793"); grin.setAttribute("height", "130"); + enterGroup.appendChild(grin); } // Switch from idle animations to excited jump From 246255bf43c88159fda72e89cb63bda60fbcc19c Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Thu, 20 Aug 2026 23:46:19 +0200 Subject: [PATCH 29/43] fix(onboarding): use the same smile as the opening screen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The transition splash now uses the exact same 3-rect pixelated smile from the welcome animation (bottom bar + two corner squares). Removed the separate grin addition from show-transition — the original smile group is already in the SVG. --- packages/extension/src/chat_panel.ts | 6 ++++-- packages/extension/src/onboarding_webview.ts | 10 +--------- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/packages/extension/src/chat_panel.ts b/packages/extension/src/chat_panel.ts index d88ede70..98d5e007 100644 --- a/packages/extension/src/chat_panel.ts +++ b/packages/extension/src/chat_panel.ts @@ -516,8 +516,10 @@ export class ChatPanel { - - + + + + diff --git a/packages/extension/src/onboarding_webview.ts b/packages/extension/src/onboarding_webview.ts index e8785dea..0d3c33cd 100644 --- a/packages/extension/src/onboarding_webview.ts +++ b/packages/extension/src/onboarding_webview.ts @@ -929,16 +929,8 @@ window.addEventListener("message", (event) => { if (ring) ring.style.animation = "none"; } - // Find the inner-most animated group to append the mouth + // Find the inner-most animated group to switch animation const enterGroup = svg.querySelector(".mark-enter") || svg.querySelector(".mark-breathe"); - if (enterGroup) { - // Add wide grin (single bar, no corners) - const ns = "http://www.w3.org/2000/svg"; - const grin = document.createElementNS(ns, "rect"); - grin.setAttribute("x", "1430"); grin.setAttribute("y", "2150"); - grin.setAttribute("width", "793"); grin.setAttribute("height", "130"); - enterGroup.appendChild(grin); - } // Switch from idle animations to excited jump const breatheGroup = svg.querySelector(".mark-breathe") as HTMLElement; From aa530ed14a5917089e1ae6c004c255e377a48895 Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Thu, 20 Aug 2026 23:51:11 +0200 Subject: [PATCH 30/43] fix(onboarding): wider smile on transition splash --- packages/extension/src/chat_panel.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/extension/src/chat_panel.ts b/packages/extension/src/chat_panel.ts index 98d5e007..a0b58c7a 100644 --- a/packages/extension/src/chat_panel.ts +++ b/packages/extension/src/chat_panel.ts @@ -516,10 +516,10 @@ export class ChatPanel { - - - - + + + + From b981500722755f3105b66a87c23b6de01c3caeec Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Thu, 20 Aug 2026 23:56:45 +0200 Subject: [PATCH 31/43] =?UTF-8?q?fix(onboarding):=20eliminate=20flash=20?= =?UTF-8?q?=E2=80=94=20direct=20HTML=20swap,=20same=20smile=20as=20opening?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The flash between two different robots is gone. Instead of posting 'show-transition' to the webview (which did imperfect DOM manipulation), the host now directly sets panel.webview.html to a static splash HTML. The splash uses the EXACT same smile as the opening screen (original coordinates, not shifted). Only the eyes differ (∩ instead of hollow squares, centered lower in the bracket). When adopt() fires, its overlay has the same SVG + CSS → same pixels → no visible switch. --- packages/extension/src/chat_panel.ts | 26 +++---- packages/extension/src/onboarding_panel.ts | 76 +++++++++++++++++-- .../extension/test/onboarding_panel.test.ts | 8 +- 3 files changed, 87 insertions(+), 23 deletions(-) diff --git a/packages/extension/src/chat_panel.ts b/packages/extension/src/chat_panel.ts index a0b58c7a..f86e2f8c 100644 --- a/packages/extension/src/chat_panel.ts +++ b/packages/extension/src/chat_panel.ts @@ -506,20 +506,20 @@ export class ChatPanel { - - - - + + + + - - - - - - - - - + + + + + + + + + diff --git a/packages/extension/src/onboarding_panel.ts b/packages/extension/src/onboarding_panel.ts index d456b2fc..1b2f237b 100644 --- a/packages/extension/src/onboarding_panel.ts +++ b/packages/extension/src/onboarding_panel.ts @@ -351,6 +351,71 @@ export function releaseOnboardingPanel(): void { currentPanel = undefined; } +/** Static splash HTML — the happy robot + "Getting Amico ready..." on a plain + * background. Used as an immediate visual while the server restarts. The exact + * same SVG + CSS appears in ChatPanel.renderTransitionHtml's overlay, so when + * adopt() fires there's no visible flash (same pixels). */ +function splashHtml(): string { + return ` + + + + + + + + + + + + + + + + + + + + + + + + + +
Getting Amico ready...
+`; +} + /** Register the onboarding panel command. Call from extension.ts activate(). */ export function registerOnboardingPanel(ctx: vscode.ExtensionContext): void { ctx.subscriptions.push( @@ -388,8 +453,8 @@ export function registerOnboardingPanel(ctx: vscode.ExtensionContext): void { } else if (msg.type === "config-success") { const payload = msg.payload as OnboardingConfig; writeOnboardingConfig(payload); - // Keep the panel alive as a transition splash (same as confirm-import) - void panel.webview.postMessage({ type: "show-transition" }); + // Swap the panel HTML directly to the splash (same as confirm-import) + panel.webview.html = splashHtml(); // Signal that the next chat panel open should auto-send the onboarding greeting ChatPanel.setPendingOnboardingGreeting(true); fireOnboardingComplete(); @@ -478,9 +543,10 @@ export function registerOnboardingPanel(ctx: vscode.ExtensionContext): void { } heldCredentials = []; testResults.clear(); - // Keep the panel alive as a transition splash — tell the webview to - // show the "Getting Amico ready..." state instead of disposing now. - void panel.webview.postMessage({ type: "show-transition" }); + // Swap the panel HTML directly to the splash — no webview-side + // DOM manipulation, so there's no flash when adopt() fires later + // (adopt's overlay uses the exact same SVG + CSS). + panel.webview.html = splashHtml(); // Signal that the next chat panel open should auto-send the onboarding greeting ChatPanel.setPendingOnboardingGreeting(true); fireOnboardingComplete(); diff --git a/packages/extension/test/onboarding_panel.test.ts b/packages/extension/test/onboarding_panel.test.ts index 27a58532..a738c27a 100644 --- a/packages/extension/test/onboarding_panel.test.ts +++ b/packages/extension/test/onboarding_panel.test.ts @@ -509,11 +509,9 @@ describe("Credential import — panel message handling (AC2, AC8, AC12, AC14)", // Panel should NOT have been disposed yet — it's showing the transition splash expect(disposeSpy).not.toHaveBeenCalled(); - // Instead, the webview should have been told to show the transition state - const transitionMsg = postSpy.mock.calls - .map((c: unknown[]) => c[0]) - .find((m: { type: string }) => m.type === "show-transition"); - expect(transitionMsg).toBeDefined(); + // Instead, the panel HTML should have been swapped to the splash + expect(panel.webview.html).toContain("Getting Amico ready"); + expect(panel.webview.html).toContain("splash-mark"); spy.mockRestore(); }); From cab8d88af863db234b16cee4f585caaa78b5ea1a Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Thu, 20 Aug 2026 23:58:30 +0200 Subject: [PATCH 32/43] fix(onboarding): splash text matches opening screen size (1.4rem, foreground color) --- packages/extension/src/chat_panel.ts | 4 ++-- packages/extension/src/onboarding_panel.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/extension/src/chat_panel.ts b/packages/extension/src/chat_panel.ts index f86e2f8c..396dc5b0 100644 --- a/packages/extension/src/chat_panel.ts +++ b/packages/extension/src/chat_panel.ts @@ -469,8 +469,8 @@ export class ChatPanel { opacity: 0; transform: scale(1.05); pointer-events: none; } .splash-text { - margin-top: 24px; font-size: 14px; - color: var(--vscode-descriptionForeground, #999); + margin-top: 16px; font-size: 1.4rem; + color: var(--vscode-foreground, #ccc); font-family: var(--vscode-font-family, system-ui); } .splash-mark { diff --git a/packages/extension/src/onboarding_panel.ts b/packages/extension/src/onboarding_panel.ts index 1b2f237b..653c01ac 100644 --- a/packages/extension/src/onboarding_panel.ts +++ b/packages/extension/src/onboarding_panel.ts @@ -385,8 +385,8 @@ function splashHtml(): string { 88%, 100% { transform: translateY(0) scale(1, 1); } } .splash-text { - margin-top: 24px; font-size: 14px; - color: var(--vscode-descriptionForeground, #999); + margin-top: 16px; font-size: 1.4rem; + color: var(--vscode-foreground, #ccc); font-family: var(--vscode-font-family, system-ui); } From ff10578ba895bd3716a7e0f704a9680c3aabfc80 Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Fri, 21 Aug 2026 00:04:23 +0200 Subject: [PATCH 33/43] =?UTF-8?q?fix(onboarding):=20prompt=20text=20?= =?UTF-8?q?=E2=86=92=20"Let's=20begin=20onboarding."?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/extension/src/chat_panel.ts | 2 +- packages/extension/src/extension.ts | 4 ++-- packages/extension/test/chat_panel.test.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/extension/src/chat_panel.ts b/packages/extension/src/chat_panel.ts index 396dc5b0..010434c3 100644 --- a/packages/extension/src/chat_panel.ts +++ b/packages/extension/src/chat_panel.ts @@ -144,7 +144,7 @@ export class ChatPanel { * must be mounted to handle the navigate). Falls back to a timeout if * app-ready never fires. */ postOnboardingGreeting(timeoutMs = 10_000): void { - const prompt = encodeURIComponent("Begin onboarding"); + const prompt = encodeURIComponent("Let's begin onboarding."); const path = `/new-session?prompt=${prompt}&autoSend=1`; const envelope = { source: "amicode", kind: "navigate", path }; let sent = false; diff --git a/packages/extension/src/extension.ts b/packages/extension/src/extension.ts index 25ee3221..a8498f87 100644 --- a/packages/extension/src/extension.ts +++ b/packages/extension/src/extension.ts @@ -207,7 +207,7 @@ async function refreshDeviceInspector(channel: vscode.OutputChannel): Promise(); -/** Create a session and arm it with "Begin onboarding" via the server API. +/** Create a session and arm it with "Let's begin onboarding." via the server API. * Bypasses the UI model gate (the server resolves its own default model). * Returns the session ID on success, undefined on failure. */ async function armOnboardingSession( @@ -231,7 +231,7 @@ async function armOnboardingSession( const commandRes = await fetch(commandUrl.toString(), { method: "POST", headers: { "Content-Type": "application/json", ...authHeaders }, - body: JSON.stringify({ command: "Begin onboarding", arguments: "" }), + body: JSON.stringify({ command: "Let's begin onboarding.", arguments: "" }), }); if (!commandRes.ok) return undefined; return id; diff --git a/packages/extension/test/chat_panel.test.ts b/packages/extension/test/chat_panel.test.ts index cf9fb579..91c23b5a 100644 --- a/packages/extension/test/chat_panel.test.ts +++ b/packages/extension/test/chat_panel.test.ts @@ -163,7 +163,7 @@ describe("ChatPanel — onboarding greeting auto-send (#449)", () => { expect(navigateMsg).toBeDefined(); expect(navigateMsg!.path).toContain("/new-session"); expect(navigateMsg!.path).toContain("autoSend=1"); - expect(navigateMsg!.path).toContain("prompt=" + encodeURIComponent("Begin onboarding")); + expect(navigateMsg!.path).toContain("prompt=" + encodeURIComponent("Let's begin onboarding.")); }); it("does NOT post navigate when postOnboardingGreeting was not called (even after app-ready)", async () => { From f6948d140c027a1bf81c434392d6e0b2926cff1c Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Fri, 21 Aug 2026 00:15:06 +0200 Subject: [PATCH 34/43] =?UTF-8?q?fix(agents):=20identity=20=E2=86=92=20're?= =?UTF-8?q?search=20copilot';=20onset=20router=20skips=20menu=20on=20onboa?= =?UTF-8?q?rding=20prompt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/extension/AGENTS.md | 10 +++++----- packages/extension/src/scores/router.ts | 7 ++++++- .../extension/test/scores/golden/router-section.md | 9 +++++++-- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/packages/extension/AGENTS.md b/packages/extension/AGENTS.md index 7688eea9..f3731841 100644 --- a/packages/extension/AGENTS.md +++ b/packages/extension/AGENTS.md @@ -2,14 +2,14 @@ ## Identity -You are **Amico** — Amicode's pulse-design copilot. You are NOT "opencode": +You are **Amico** — Amicode's research copilot. You are NOT "opencode": opencode is the engine underneath, **Amicode** is the product, **Amico** is you. If asked who or what you are, answer in one line — "I'm Amico — Amicode's -pulse-design copilot" — and never describe yourself as an interactive CLI tool. +research copilot" — and never describe yourself as an interactive CLI tool. -You help a quantum-control researcher synthesize optimal-control pulses with -Piccolo (Julia) without leaving VS Code. You author a Julia script, run it, -and the Run Inspector renders the live solve. +You help researchers design and optimize quantum-control pulses, write code, +and run experiments — without leaving VS Code. You author Julia scripts, run +them, and the Run Inspector renders the live solve. ## Voice diff --git a/packages/extension/src/scores/router.ts b/packages/extension/src/scores/router.ts index 2c52fc01..1c80fe1a 100644 --- a/packages/extension/src/scores/router.ts +++ b/packages/extension/src/scores/router.ts @@ -15,7 +15,12 @@ export function buildRouterSection(visible: Score[]): string { const lines: string[] = [ "## Onset router", "", - "When a session opens without a specific request (a greeting, \"who are", + 'When a session opens with an explicit onboarding request ("Let\'s begin', + 'onboarding", "begin onboarding", "start onboarding", or similar), **skip this', + "router entirely** and go straight into the overture (Stage 1 below) — the user", + "has already chosen.", + "", + "Otherwise, when a session opens without a specific request (a greeting, \"who are", "you?\", \"what is this?\"), do NOT default to the pulse-designer interview —", "build the moment from the live state. After your one-line Amico intro (name from", "the profile when one is recorded), ask exactly ONE question —", diff --git a/packages/extension/test/scores/golden/router-section.md b/packages/extension/test/scores/golden/router-section.md index fca1bef2..d284234f 100644 --- a/packages/extension/test/scores/golden/router-section.md +++ b/packages/extension/test/scores/golden/router-section.md @@ -1,6 +1,11 @@ ## Onset router -When a session opens without a specific request (a greeting, "who are +When a session opens with an explicit onboarding request ("Let's begin +onboarding", "begin onboarding", "start onboarding", or similar), **skip this +router entirely** and go straight into the overture (Stage 1 below) — the user +has already chosen. + +Otherwise, when a session opens without a specific request (a greeting, "who are you?", "what is this?"), do NOT default to the pulse-designer interview — build the moment from the live state. After your one-line Amico intro (name from the profile when one is recorded), ask exactly ONE question — @@ -23,4 +28,4 @@ fleet option with the application entry cards: Never a dead end: if nothing usable is found for an option, say so and offer the others. If candidates match multiple paths equally, ask — never route by silent heuristic. A user who opens with a specific ask ("X gate, 10 ns, -defaults") skips the question entirely and gets straight to it. \ No newline at end of file +defaults") skips the question entirely and gets straight to it. From 0b3ab803c03f77810eca067b26e8944d22bc8ad7 Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Fri, 21 Aug 2026 00:22:59 +0200 Subject: [PATCH 35/43] =?UTF-8?q?fix(overture):=20de-quantum=20onboarding?= =?UTF-8?q?=20=E2=80=94=20generic=20language,=20skip=20env/devices=20for?= =?UTF-8?q?=20non-research=20users?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/extension/scores/overture/SCORE.md | 72 +++++++++---------- .../extension/test/scores/compiler.test.ts | 2 +- .../test/scores/golden/compile-chained.md | 65 +++++++++-------- 3 files changed, 69 insertions(+), 70 deletions(-) diff --git a/packages/extension/scores/overture/SCORE.md b/packages/extension/scores/overture/SCORE.md index 8cf36678..0ecfe6da 100644 --- a/packages/extension/scores/overture/SCORE.md +++ b/packages/extension/scores/overture/SCORE.md @@ -22,11 +22,11 @@ stages: choices: [ "General coding and software development", - "Research", + "Perform (automated) experiments and gain scientific insights", "Exploring", ] multiple: true - default: "Research" + default: "Perform (automated) experiments and gain scientific insights" - id: context_seed optional: true questions: @@ -42,13 +42,14 @@ stages: choices: ["Yes, show me", "Skip the demo"] default: "Yes, show me" - id: environment + optional: true questions: - id: environment - prompt: "How will pulses eventually reach hardware — what are we patching into?" + prompt: "How will your experiments reach hardware?" choices: [ - "QICK lab (on-prem control code)", - "Cloud system with emulator (e.g. Pasqal)", + "Lab hardware (on-prem control system)", + "Cloud platform with emulator", "Simulation only for now", "Something else", ] @@ -57,7 +58,7 @@ stages: optional: true questions: - id: devices - prompt: "Any specific device(s) you want me to remember? (name, platform, qubit count — or skip)" + prompt: "Any specific device(s) you want me to remember? (name, platform, specs — or skip)" default: "skip for now" - id: goals questions: @@ -68,8 +69,8 @@ stages: questions: - id: handoff prompt: "Ready to get started?" - choices: ["Walk me through designing a pulse", "Open a normal session", "Show me around first"] - default: "Walk me through designing a pulse" + choices: ["Let's dive into my first task", "Open a normal session", "Show me around first"] + default: "Let's dive into my first task" --- You are running the **overture** — Amico's onboarding interview (session zero). @@ -102,10 +103,9 @@ Per-stage guidance and the `amicode_profile` mapping: **What Amicode is (share naturally within this greeting, not as a lecture):** Amicode is a general-purpose agentic coding assistant AND a research studio. - It remembers context across sessions, runs optimization solves, manages - experiments, and adapts to your workflow — whether that's writing code, - designing pulses, or exploring what's possible. It is NOT solely a quantum - control tool, though that's one of its deep specialties. + It remembers context across sessions, runs automated experiments, manages + results, and adapts to your workflow — whether that's writing code, running + optimizations, or exploring what's possible. Do NOT ask about experience level. Do NOT branch by expertise. The same warm, brief orientation for everyone. @@ -114,7 +114,7 @@ Per-stage guidance and the `amicode_profile` mapping: `multiple: true`. The question: "What brings you to Amicode?" with exactly three options: - "General coding and software development" - - "Research" + - "Perform (automated) experiments and gain scientific insights" - "Exploring" The user may select any combination (1, 2, or all 3). Record: @@ -122,9 +122,9 @@ Per-stage guidance and the `amicode_profile` mapping: Use lowercase slug forms in the array: `research`, `general_coding`, `exploring`. **DO NOT ask research sub-type here.** Platform, problem type, and domain - specifics are deferred entirely to the pulse-designer interview — they will - be asked when the user starts a research task, not during onboarding. This - keeps the overture fast and generic. + specifics are deferred to later — they will be asked when the user starts + their first research task, not during onboarding. This keeps the overture + fast and generic. After recording intent, acknowledge briefly ("Got it — let's get you set up") and advance to Stage 3. @@ -166,11 +166,10 @@ Per-stage guidance and the `amicode_profile` mapping: 4. **demo** _(optional)_ — check Julia readiness by calling `amicode_demo_check`. This returns `{ready: true|false, reason?}`. - **If ready:** offer the demo: "Let me show you the full workflow end-to-end - — I'll run a quick transmon X-gate optimization so you can see the entity - strip, the Run Inspector, and a converging pulse." Frame it as a WORKFLOW - SHOWCASE, not a quantum-specific exercise — it works for all intent - selections. + **If ready:** offer the demo: "Want me to run a quick optimization demo so + you can see the workflow end-to-end — the Run Inspector, live iterations, + and a converging result?" Frame it as a WORKFLOW SHOWCASE, not a + domain-specific exercise — it works for all intent selections. On accept, call `amicode_demo_launch`. This creates a `__demo__` workspace, fills the vetted template with stock parameters (T=10ns, N=50, max_iter=60), @@ -195,22 +194,23 @@ Per-stage guidance and the `amicode_profile` mapping: After the demo (or skipping), advance to Stage 5. -5. **environment** — ask how pulses will reach hardware. **Pre-fill from - seeds:** call `amicode_profile {entity:"status"}` and check if an - environment is already recorded from the context-seed (Stage 3). If so, - present it as a confirmation: "I found you use {archetype} — confirm, or - change?" via the `question` tool. If no seed, ask the standard choice - question with the options above. +5. **environment** — _(only if user selected the experiments intent)_ — ask how + experiments will reach hardware. **Pre-fill from seeds:** call + `amicode_profile {entity:"status"}` and check if an environment is already + recorded from the context-seed (Stage 3). If so, present it as a + confirmation: "I found you use {archetype} — confirm, or change?" via the + `question` tool. If no seed, ask the standard choice question with the + options above. Record: `amicode_profile {entity:"environment", payload:{slug, archetype}}`. - Follow up on details per archetype if confirmed (QICK: tProc version, - repo pointer; cloud-Pasqal: which provider, emulator access; etc.). + Follow up on details per archetype if confirmed. -6. **devices** _(optional)_ — same pre-fill pattern: if a device was seeded, - confirm it. Otherwise ask: "Any specific device(s) you want me to remember?" +6. **devices** _(optional, only if user selected the experiments intent)_ — + same pre-fill pattern: if a device was seeded, confirm it. Otherwise ask: + "Any specific device(s) you want me to remember?" This stage is ALWAYS skippable — "none" or "skip" is a valid answer. - Record: `amicode_profile {entity:"device", payload:{name, platform, qubits}}`. + Record: `amicode_profile {entity:"device", payload:{name, platform, specs}}`. If skipped, move on without recording. 7. **goals** — free-text question via `question` tool with `kind: "text"`: @@ -227,10 +227,10 @@ Per-stage guidance and the `amicode_profile` mapping: Then route by the user's intent selections (from Stage 2 — read from the events stream, do NOT re-ask): - - **Research** selected (alone or combined) → "Let's design your first - pulse" → continue straight into the **pulse-designer interview** in this - same session. Use everything learned (platform, environment, device) to - skip pulse-design questions already answered. + - **Research/experiments** selected (alone or combined) → "Let's set up your + first experiment" → continue straight into the **pulse-designer interview** + in this same session. Use everything learned (environment, device) to + skip questions already answered. - **Research + General coding** → same as above, but acknowledge: "I'm also your general coding companion — you can switch modes any time." - **General coding only** (no Research) → open a normal session: "You're all diff --git a/packages/extension/test/scores/compiler.test.ts b/packages/extension/test/scores/compiler.test.ts index f8c84b89..1c8c52ea 100644 --- a/packages/extension/test/scores/compiler.test.ts +++ b/packages/extension/test/scores/compiler.test.ts @@ -82,7 +82,7 @@ describe("compileChainedScore (real overture → pulse-designer)", () => { expect(md).not.toContain("plain text"); }); it("keeps the overture's choice questions as option cards, default first (amicode#245 AC6 regression)", () => { - expect(md).toContain("Research (recommended)"); + expect(md).toContain("Perform (automated) experiments and gain scientific insights (recommended)"); expect(md).toContain("General coding and software development"); }); }); diff --git a/packages/extension/test/scores/golden/compile-chained.md b/packages/extension/test/scores/golden/compile-chained.md index 844ca185..1b9580b4 100644 --- a/packages/extension/test/scores/golden/compile-chained.md +++ b/packages/extension/test/scores/golden/compile-chained.md @@ -15,19 +15,19 @@ gate's checks pass. 1. **orientation** - Q `name`: "What should I call you?" 2. **intent** - - Q `intent`: "What brings you to Amicode?" — options: General coding and software development | Research (recommended) | Exploring + - Q `intent`: "What brings you to Amicode?" — options: General coding and software development | Perform (automated) experiments and gain scientific insights (recommended) | Exploring 3. **context_seed** (optional) - Q `seed_optin`: "I can scan your existing AI-tool configs to bootstrap your workspace — want me to?" — options: Yes, scan my configs (recommended) | No thanks, skip 4. **demo** (optional) - Q `demo_offer`: "Want me to show you the full workflow end-to-end? (requires Julia)" — options: Yes, show me (recommended) | Skip the demo -5. **environment** - - Q `environment`: "How will pulses eventually reach hardware — what are we patching into?" — options: QICK lab (on-prem control code) | Cloud system with emulator (e.g. Pasqal) | Simulation only for now (recommended) | Something else +5. **environment** (optional) + - Q `environment`: "How will your experiments reach hardware?" — options: Lab hardware (on-prem control system) | Cloud platform with emulator | Simulation only for now (recommended) | Something else 6. **devices** (optional) - - Q `devices`: "Any specific device(s) you want me to remember? (name, platform, qubit count — or skip)" — default: skip for now + - Q `devices`: "Any specific device(s) you want me to remember? (name, platform, specs — or skip)" — default: skip for now 7. **goals** - Q `goals`: "What are you hoping to accomplish with Amico?" 8. **handoff** - - Q `handoff`: "Ready to get started?" — options: Walk me through designing a pulse (recommended) | Open a normal session | Show me around first + - Q `handoff`: "Ready to get started?" — options: Let's dive into my first task (recommended) | Open a normal session | Show me around first 9. **platform** - Q `platform`: "What kind of system are you working with?" — options: transmon (recommended) | neutral-atom Rydberg | cavity / bosonic | other 10. **model** @@ -85,10 +85,9 @@ Per-stage guidance and the `amicode_profile` mapping: **What Amicode is (share naturally within this greeting, not as a lecture):** Amicode is a general-purpose agentic coding assistant AND a research studio. - It remembers context across sessions, runs optimization solves, manages - experiments, and adapts to your workflow — whether that's writing code, - designing pulses, or exploring what's possible. It is NOT solely a quantum - control tool, though that's one of its deep specialties. + It remembers context across sessions, runs automated experiments, manages + results, and adapts to your workflow — whether that's writing code, running + optimizations, or exploring what's possible. Do NOT ask about experience level. Do NOT branch by expertise. The same warm, brief orientation for everyone. @@ -97,7 +96,7 @@ Per-stage guidance and the `amicode_profile` mapping: `multiple: true`. The question: "What brings you to Amicode?" with exactly three options: - "General coding and software development" - - "Research" + - "Perform (automated) experiments and gain scientific insights" - "Exploring" The user may select any combination (1, 2, or all 3). Record: @@ -105,9 +104,9 @@ Per-stage guidance and the `amicode_profile` mapping: Use lowercase slug forms in the array: `research`, `general_coding`, `exploring`. **DO NOT ask research sub-type here.** Platform, problem type, and domain - specifics are deferred entirely to the pulse-designer interview — they will - be asked when the user starts a research task, not during onboarding. This - keeps the overture fast and generic. + specifics are deferred to later — they will be asked when the user starts + their first research task, not during onboarding. This keeps the overture + fast and generic. After recording intent, acknowledge briefly ("Got it — let's get you set up") and advance to Stage 3. @@ -149,11 +148,10 @@ Per-stage guidance and the `amicode_profile` mapping: 4. **demo** _(optional)_ — check Julia readiness by calling `amicode_demo_check`. This returns `{ready: true|false, reason?}`. - **If ready:** offer the demo: "Let me show you the full workflow end-to-end - — I'll run a quick transmon X-gate optimization so you can see the entity - strip, the Run Inspector, and a converging pulse." Frame it as a WORKFLOW - SHOWCASE, not a quantum-specific exercise — it works for all intent - selections. + **If ready:** offer the demo: "Want me to run a quick optimization demo so + you can see the workflow end-to-end — the Run Inspector, live iterations, + and a converging result?" Frame it as a WORKFLOW SHOWCASE, not a + domain-specific exercise — it works for all intent selections. On accept, call `amicode_demo_launch`. This creates a `__demo__` workspace, fills the vetted template with stock parameters (T=10ns, N=50, max_iter=60), @@ -178,22 +176,23 @@ Per-stage guidance and the `amicode_profile` mapping: After the demo (or skipping), advance to Stage 5. -5. **environment** — ask how pulses will reach hardware. **Pre-fill from - seeds:** call `amicode_profile {entity:"status"}` and check if an - environment is already recorded from the context-seed (Stage 3). If so, - present it as a confirmation: "I found you use {archetype} — confirm, or - change?" via the `question` tool. If no seed, ask the standard choice - question with the options above. +5. **environment** — _(only if user selected the experiments intent)_ — ask how + experiments will reach hardware. **Pre-fill from seeds:** call + `amicode_profile {entity:"status"}` and check if an environment is already + recorded from the context-seed (Stage 3). If so, present it as a + confirmation: "I found you use {archetype} — confirm, or change?" via the + `question` tool. If no seed, ask the standard choice question with the + options above. Record: `amicode_profile {entity:"environment", payload:{slug, archetype}}`. - Follow up on details per archetype if confirmed (QICK: tProc version, - repo pointer; cloud-Pasqal: which provider, emulator access; etc.). + Follow up on details per archetype if confirmed. -6. **devices** _(optional)_ — same pre-fill pattern: if a device was seeded, - confirm it. Otherwise ask: "Any specific device(s) you want me to remember?" +6. **devices** _(optional, only if user selected the experiments intent)_ — + same pre-fill pattern: if a device was seeded, confirm it. Otherwise ask: + "Any specific device(s) you want me to remember?" This stage is ALWAYS skippable — "none" or "skip" is a valid answer. - Record: `amicode_profile {entity:"device", payload:{name, platform, qubits}}`. + Record: `amicode_profile {entity:"device", payload:{name, platform, specs}}`. If skipped, move on without recording. 7. **goals** — free-text question via `question` tool with `kind: "text"`: @@ -210,10 +209,10 @@ Per-stage guidance and the `amicode_profile` mapping: Then route by the user's intent selections (from Stage 2 — read from the events stream, do NOT re-ask): - - **Research** selected (alone or combined) → "Let's design your first - pulse" → continue straight into the **pulse-designer interview** in this - same session. Use everything learned (platform, environment, device) to - skip pulse-design questions already answered. + - **Research/experiments** selected (alone or combined) → "Let's set up your + first experiment" → continue straight into the **pulse-designer interview** + in this same session. Use everything learned (environment, device) to + skip questions already answered. - **Research + General coding** → same as above, but acknowledge: "I'm also your general coding companion — you can switch modes any time." - **General coding only** (no Research) → open a normal session: "You're all From e3c435ed13f9ad4d04e2aa9c943da295ecccb807 Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Fri, 21 Aug 2026 00:26:29 +0200 Subject: [PATCH 36/43] feat(overture): add research_area free-form question after intent selection --- packages/extension/scores/overture/SCORE.md | 37 ++++++----- .../test/scores/golden/compile-chained.md | 61 ++++++++++--------- .../test/scores/overture_rewrite.test.ts | 4 +- 3 files changed, 58 insertions(+), 44 deletions(-) diff --git a/packages/extension/scores/overture/SCORE.md b/packages/extension/scores/overture/SCORE.md index 0ecfe6da..fee298ab 100644 --- a/packages/extension/scores/overture/SCORE.md +++ b/packages/extension/scores/overture/SCORE.md @@ -27,6 +27,12 @@ stages: ] multiple: true default: "Perform (automated) experiments and gain scientific insights" + - id: research_area + optional: true + questions: + - id: research_area + prompt: "What research area and what kind of experiments?" + kind: text - id: context_seed optional: true questions: @@ -121,15 +127,18 @@ Per-stage guidance and the `amicode_profile` mapping: `amicode_profile {entity:"profile", payload:{intent:["research","general_coding","exploring"]}}`. Use lowercase slug forms in the array: `research`, `general_coding`, `exploring`. - **DO NOT ask research sub-type here.** Platform, problem type, and domain - specifics are deferred to later — they will be asked when the user starts - their first research task, not during onboarding. This keeps the overture - fast and generic. - After recording intent, acknowledge briefly ("Got it — let's get you set up") - and advance to Stage 3. + and advance. + +3. **research_area** _(optional — only if user selected the experiments intent)_ — + ask via the `question` tool with `kind: "text"`: "What research area and what + kind of experiments?" This is free-form — the user can say anything from + "quantum optimal control for transmon gates" to "protein folding simulations" + to "materials science DFT sweeps." Record whatever they say: + `amicode_profile {entity:"profile", payload:{research_area:"..."}}`. + If the user didn't select the experiments intent, skip this stage entirely. -3. **context_seed** _(optional)_ — offer an explicit opt-in: "I can scan your +4. **context_seed** _(optional)_ — offer an explicit opt-in: "I can scan your existing AI-tool configs (CLAUDE.md, cursor rules, opencode config) to bootstrap your workspace — want me to?" via the `question` tool with the two choices above. @@ -161,9 +170,9 @@ Per-stage guidance and the `amicode_profile` mapping: - If no scannable files are found, say so honestly: "I didn't find any AI-tool configs to import — no worries, we'll build your context as we go." - After seeding (or declining), advance to Stage 4 (demo). + After seeding (or declining), advance to Stage 5 (demo). -4. **demo** _(optional)_ — check Julia readiness by calling +5. **demo** _(optional)_ — check Julia readiness by calling `amicode_demo_check`. This returns `{ready: true|false, reason?}`. **If ready:** offer the demo: "Want me to run a quick optimization demo so @@ -192,9 +201,9 @@ Per-stage guidance and the `amicode_profile` mapping: - The demo MUST NOT create vault artifacts (no problem card, no pulse bank entry). - If `isDemoCompleted()` is true (archive marker exists), skip — don't re-offer. - After the demo (or skipping), advance to Stage 5. + After the demo (or skipping), advance to Stage 6. -5. **environment** — _(only if user selected the experiments intent)_ — ask how +6. **environment** — _(only if user selected the experiments intent)_ — ask how experiments will reach hardware. **Pre-fill from seeds:** call `amicode_profile {entity:"status"}` and check if an environment is already recorded from the context-seed (Stage 3). If so, present it as a @@ -205,7 +214,7 @@ Per-stage guidance and the `amicode_profile` mapping: Record: `amicode_profile {entity:"environment", payload:{slug, archetype}}`. Follow up on details per archetype if confirmed. -6. **devices** _(optional, only if user selected the experiments intent)_ — +7. **devices** _(optional, only if user selected the experiments intent)_ — same pre-fill pattern: if a device was seeded, confirm it. Otherwise ask: "Any specific device(s) you want me to remember?" This stage is ALWAYS skippable — "none" or "skip" is a valid answer. @@ -213,13 +222,13 @@ Per-stage guidance and the `amicode_profile` mapping: Record: `amicode_profile {entity:"device", payload:{name, platform, specs}}`. If skipped, move on without recording. -7. **goals** — free-text question via `question` tool with `kind: "text"`: +8. **goals** — free-text question via `question` tool with `kind: "text"`: "What are you hoping to accomplish with Amico?" No pre-fill (goals are personal, not inferrable from configs). Record: `amicode_profile {entity:"profile", payload:{goals:"..."}}`. -8. **handoff** — the terminal stage. FIRST, record the completion marker: +9. **handoff** — the terminal stage. FIRST, record the completion marker: `amicode_profile {entity:"onboarding_completed"}` (exactly once — this is what lets Amico remember them next time and triggers the distiller to materialize the vault). diff --git a/packages/extension/test/scores/golden/compile-chained.md b/packages/extension/test/scores/golden/compile-chained.md index 1b9580b4..cc2f8e5b 100644 --- a/packages/extension/test/scores/golden/compile-chained.md +++ b/packages/extension/test/scores/golden/compile-chained.md @@ -16,41 +16,43 @@ gate's checks pass. - Q `name`: "What should I call you?" 2. **intent** - Q `intent`: "What brings you to Amicode?" — options: General coding and software development | Perform (automated) experiments and gain scientific insights (recommended) | Exploring -3. **context_seed** (optional) +3. **research_area** (optional) + - Q `research_area`: "What research area and what kind of experiments?" +4. **context_seed** (optional) - Q `seed_optin`: "I can scan your existing AI-tool configs to bootstrap your workspace — want me to?" — options: Yes, scan my configs (recommended) | No thanks, skip -4. **demo** (optional) +5. **demo** (optional) - Q `demo_offer`: "Want me to show you the full workflow end-to-end? (requires Julia)" — options: Yes, show me (recommended) | Skip the demo -5. **environment** (optional) +6. **environment** (optional) - Q `environment`: "How will your experiments reach hardware?" — options: Lab hardware (on-prem control system) | Cloud platform with emulator | Simulation only for now (recommended) | Something else -6. **devices** (optional) +7. **devices** (optional) - Q `devices`: "Any specific device(s) you want me to remember? (name, platform, specs — or skip)" — default: skip for now -7. **goals** +8. **goals** - Q `goals`: "What are you hoping to accomplish with Amico?" -8. **handoff** +9. **handoff** - Q `handoff`: "Ready to get started?" — options: Let's dive into my first task (recommended) | Open a normal session | Show me around first -9. **platform** +10. **platform** - Q `platform`: "What kind of system are you working with?" — options: transmon (recommended) | neutral-atom Rydberg | cavity / bosonic | other -10. **model** +11. **model** - emits: system — record via the matching `amicode_*` tool - Q `levels`: "How many levels should the model keep? (I'll recommend based on your system — see guidance)" — default: platform-dependent (transmon 3–4; a cavity/bosonic mode wants a Fock cutoff) - Q `drives`: "Drive parameterization and amplitude bound (drive_max)?" — default: two quadratures, drive_max = 0.2 GHz -11. **mode** +12. **mode** - Q `mode`: "Simulate first, or go straight to solve?" — options: solve (recommended) | simulate - Q `warm_start`: "Warm start from a previous pulse (pulse.jld2) — including one from your pulse bank — or cold start?" — options: cold start (recommended) | warm start - skip if: mode == simulate -12. **problem** +13. **problem** - Q `target`: "What is the target — a gate, or a state to prepare?" — default: a single-qubit gate -13. **formulate** +14. **formulate** - emits: formulation — record via the matching `amicode_*` tool - Q `formulation`: "The problem shape — trajectory type (gate / state-prep / open-system), fixed-time vs min-time, and any robustness or free-phase? (the infidelity objective is DERIVED from the type; constraints default to the amplitude bound)" — default: a fixed-time gate, free-phase on for entangling gates - [Why?] hooks: free-phase-objective-only, pin-globals-first-solve (read `scores/memory/.md` on request) -14. **solve** +15. **solve** - emits: run, pulse — record via the matching `amicode_*` tool - executor: `local` - vetted template (absolute): `/extension/scores/pulse-designer/templates/solve.jl` - Q `solve_params`: "Pulse duration T (ns), timesteps N, and max_iter?" — default: T = 10 ns, N = 50, max_iter = 60 -15. **inspect** -16. **hardware** (optional) +16. **inspect** +17. **hardware** (optional) - emits: device_session — record via the matching `amicode_*` tool --- @@ -103,15 +105,18 @@ Per-stage guidance and the `amicode_profile` mapping: `amicode_profile {entity:"profile", payload:{intent:["research","general_coding","exploring"]}}`. Use lowercase slug forms in the array: `research`, `general_coding`, `exploring`. - **DO NOT ask research sub-type here.** Platform, problem type, and domain - specifics are deferred to later — they will be asked when the user starts - their first research task, not during onboarding. This keeps the overture - fast and generic. - After recording intent, acknowledge briefly ("Got it — let's get you set up") - and advance to Stage 3. + and advance. + +3. **research_area** _(optional — only if user selected the experiments intent)_ — + ask via the `question` tool with `kind: "text"`: "What research area and what + kind of experiments?" This is free-form — the user can say anything from + "quantum optimal control for transmon gates" to "protein folding simulations" + to "materials science DFT sweeps." Record whatever they say: + `amicode_profile {entity:"profile", payload:{research_area:"..."}}`. + If the user didn't select the experiments intent, skip this stage entirely. -3. **context_seed** _(optional)_ — offer an explicit opt-in: "I can scan your +4. **context_seed** _(optional)_ — offer an explicit opt-in: "I can scan your existing AI-tool configs (CLAUDE.md, cursor rules, opencode config) to bootstrap your workspace — want me to?" via the `question` tool with the two choices above. @@ -143,9 +148,9 @@ Per-stage guidance and the `amicode_profile` mapping: - If no scannable files are found, say so honestly: "I didn't find any AI-tool configs to import — no worries, we'll build your context as we go." - After seeding (or declining), advance to Stage 4 (demo). + After seeding (or declining), advance to Stage 5 (demo). -4. **demo** _(optional)_ — check Julia readiness by calling +5. **demo** _(optional)_ — check Julia readiness by calling `amicode_demo_check`. This returns `{ready: true|false, reason?}`. **If ready:** offer the demo: "Want me to run a quick optimization demo so @@ -174,9 +179,9 @@ Per-stage guidance and the `amicode_profile` mapping: - The demo MUST NOT create vault artifacts (no problem card, no pulse bank entry). - If `isDemoCompleted()` is true (archive marker exists), skip — don't re-offer. - After the demo (or skipping), advance to Stage 5. + After the demo (or skipping), advance to Stage 6. -5. **environment** — _(only if user selected the experiments intent)_ — ask how +6. **environment** — _(only if user selected the experiments intent)_ — ask how experiments will reach hardware. **Pre-fill from seeds:** call `amicode_profile {entity:"status"}` and check if an environment is already recorded from the context-seed (Stage 3). If so, present it as a @@ -187,7 +192,7 @@ Per-stage guidance and the `amicode_profile` mapping: Record: `amicode_profile {entity:"environment", payload:{slug, archetype}}`. Follow up on details per archetype if confirmed. -6. **devices** _(optional, only if user selected the experiments intent)_ — +7. **devices** _(optional, only if user selected the experiments intent)_ — same pre-fill pattern: if a device was seeded, confirm it. Otherwise ask: "Any specific device(s) you want me to remember?" This stage is ALWAYS skippable — "none" or "skip" is a valid answer. @@ -195,13 +200,13 @@ Per-stage guidance and the `amicode_profile` mapping: Record: `amicode_profile {entity:"device", payload:{name, platform, specs}}`. If skipped, move on without recording. -7. **goals** — free-text question via `question` tool with `kind: "text"`: +8. **goals** — free-text question via `question` tool with `kind: "text"`: "What are you hoping to accomplish with Amico?" No pre-fill (goals are personal, not inferrable from configs). Record: `amicode_profile {entity:"profile", payload:{goals:"..."}}`. -8. **handoff** — the terminal stage. FIRST, record the completion marker: +9. **handoff** — the terminal stage. FIRST, record the completion marker: `amicode_profile {entity:"onboarding_completed"}` (exactly once — this is what lets Amico remember them next time and triggers the distiller to materialize the vault). diff --git a/packages/extension/test/scores/overture_rewrite.test.ts b/packages/extension/test/scores/overture_rewrite.test.ts index 75725e05..765f99a9 100644 --- a/packages/extension/test/scores/overture_rewrite.test.ts +++ b/packages/extension/test/scores/overture_rewrite.test.ts @@ -110,8 +110,8 @@ describe("overture compiled content — Stage 2 intent (AC4, AC5, AC6)", () => { expect(md).toMatch(/intent.*\[.*research.*general_coding.*exploring.*\]/s); }); - it("AC6: does NOT ask research sub-type (deferred to pulse-designer)", () => { - expect(md).toContain("DO NOT ask research sub-type"); + it("AC6: asks research area as free-form, does NOT ask platform-specific sub-types", () => { + expect(md).toContain("What research area and what kind of experiments?"); expect(md).not.toContain("Which platform"); expect(md).not.toContain("qubit platforms"); }); From 5c34861e2f61b55ecb0a8d84d18c79ccfb785adb Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Fri, 21 Aug 2026 00:33:56 +0200 Subject: [PATCH 37/43] fix(overture): remove demo stage from onboarding flow --- packages/extension/scores/overture/SCORE.md | 48 ++----------- .../test/scores/golden/compile-chained.md | 67 +++++-------------- .../test/scores/overture_rewrite.test.ts | 11 +-- 3 files changed, 28 insertions(+), 98 deletions(-) diff --git a/packages/extension/scores/overture/SCORE.md b/packages/extension/scores/overture/SCORE.md index fee298ab..58cf3b07 100644 --- a/packages/extension/scores/overture/SCORE.md +++ b/packages/extension/scores/overture/SCORE.md @@ -40,13 +40,6 @@ stages: prompt: "I can scan your existing AI-tool configs to bootstrap your workspace — want me to?" choices: ["Yes, scan my configs", "No thanks, skip"] default: "Yes, scan my configs" - - id: demo - optional: true - questions: - - id: demo_offer - prompt: "Want me to show you the full workflow end-to-end? (requires Julia)" - choices: ["Yes, show me", "Skip the demo"] - default: "Yes, show me" - id: environment optional: true questions: @@ -170,40 +163,9 @@ Per-stage guidance and the `amicode_profile` mapping: - If no scannable files are found, say so honestly: "I didn't find any AI-tool configs to import — no worries, we'll build your context as we go." - After seeding (or declining), advance to Stage 5 (demo). - -5. **demo** _(optional)_ — check Julia readiness by calling - `amicode_demo_check`. This returns `{ready: true|false, reason?}`. - - **If ready:** offer the demo: "Want me to run a quick optimization demo so - you can see the workflow end-to-end — the Run Inspector, live iterations, - and a converging result?" Frame it as a WORKFLOW SHOWCASE, not a - domain-specific exercise — it works for all intent selections. - - On accept, call `amicode_demo_launch`. This creates a `__demo__` workspace, - fills the vetted template with stock parameters (T=10ns, N=50, max_iter=60), - and launches through `amico-run --spec`. The Run Inspector streams - iterations live. After FINISHED, report the result: "Solved — F=0.9998 in - 47 iterations" (or whatever the actual numbers are). Then call - `amicode_demo_archive` to clean up the ephemeral workspace. - - **If not ready:** explain honestly: "Julia environment isn't set up yet — - {reason}. No worries, we'll skip the demo. You can always run one later - from the command palette." Advance without blocking. - - **If the user DECLINES the demo:** say "No problem" and advance. - - **If the demo FAILS** (Julia error, convergence failure): report honestly - and continue. A failed demo never blocks onboarding. - - **Constraints:** - - The demo MUST use the vetted template — never free-tier. - - The demo MUST NOT create vault artifacts (no problem card, no pulse bank entry). - - If `isDemoCompleted()` is true (archive marker exists), skip — don't re-offer. - - After the demo (or skipping), advance to Stage 6. + After seeding (or declining), advance to Stage 5. -6. **environment** — _(only if user selected the experiments intent)_ — ask how +5. **environment** — _(only if user selected the experiments intent)_ — ask how experiments will reach hardware. **Pre-fill from seeds:** call `amicode_profile {entity:"status"}` and check if an environment is already recorded from the context-seed (Stage 3). If so, present it as a @@ -214,7 +176,7 @@ Per-stage guidance and the `amicode_profile` mapping: Record: `amicode_profile {entity:"environment", payload:{slug, archetype}}`. Follow up on details per archetype if confirmed. -7. **devices** _(optional, only if user selected the experiments intent)_ — +6. **devices** _(optional, only if user selected the experiments intent)_ — same pre-fill pattern: if a device was seeded, confirm it. Otherwise ask: "Any specific device(s) you want me to remember?" This stage is ALWAYS skippable — "none" or "skip" is a valid answer. @@ -222,13 +184,13 @@ Per-stage guidance and the `amicode_profile` mapping: Record: `amicode_profile {entity:"device", payload:{name, platform, specs}}`. If skipped, move on without recording. -8. **goals** — free-text question via `question` tool with `kind: "text"`: +7. **goals** — free-text question via `question` tool with `kind: "text"`: "What are you hoping to accomplish with Amico?" No pre-fill (goals are personal, not inferrable from configs). Record: `amicode_profile {entity:"profile", payload:{goals:"..."}}`. -9. **handoff** — the terminal stage. FIRST, record the completion marker: +8. **handoff** — the terminal stage. FIRST, record the completion marker: `amicode_profile {entity:"onboarding_completed"}` (exactly once — this is what lets Amico remember them next time and triggers the distiller to materialize the vault). diff --git a/packages/extension/test/scores/golden/compile-chained.md b/packages/extension/test/scores/golden/compile-chained.md index cc2f8e5b..ad06ded0 100644 --- a/packages/extension/test/scores/golden/compile-chained.md +++ b/packages/extension/test/scores/golden/compile-chained.md @@ -20,39 +20,37 @@ gate's checks pass. - Q `research_area`: "What research area and what kind of experiments?" 4. **context_seed** (optional) - Q `seed_optin`: "I can scan your existing AI-tool configs to bootstrap your workspace — want me to?" — options: Yes, scan my configs (recommended) | No thanks, skip -5. **demo** (optional) - - Q `demo_offer`: "Want me to show you the full workflow end-to-end? (requires Julia)" — options: Yes, show me (recommended) | Skip the demo -6. **environment** (optional) +5. **environment** (optional) - Q `environment`: "How will your experiments reach hardware?" — options: Lab hardware (on-prem control system) | Cloud platform with emulator | Simulation only for now (recommended) | Something else -7. **devices** (optional) +6. **devices** (optional) - Q `devices`: "Any specific device(s) you want me to remember? (name, platform, specs — or skip)" — default: skip for now -8. **goals** +7. **goals** - Q `goals`: "What are you hoping to accomplish with Amico?" -9. **handoff** +8. **handoff** - Q `handoff`: "Ready to get started?" — options: Let's dive into my first task (recommended) | Open a normal session | Show me around first -10. **platform** +9. **platform** - Q `platform`: "What kind of system are you working with?" — options: transmon (recommended) | neutral-atom Rydberg | cavity / bosonic | other -11. **model** +10. **model** - emits: system — record via the matching `amicode_*` tool - Q `levels`: "How many levels should the model keep? (I'll recommend based on your system — see guidance)" — default: platform-dependent (transmon 3–4; a cavity/bosonic mode wants a Fock cutoff) - Q `drives`: "Drive parameterization and amplitude bound (drive_max)?" — default: two quadratures, drive_max = 0.2 GHz -12. **mode** +11. **mode** - Q `mode`: "Simulate first, or go straight to solve?" — options: solve (recommended) | simulate - Q `warm_start`: "Warm start from a previous pulse (pulse.jld2) — including one from your pulse bank — or cold start?" — options: cold start (recommended) | warm start - skip if: mode == simulate -13. **problem** +12. **problem** - Q `target`: "What is the target — a gate, or a state to prepare?" — default: a single-qubit gate -14. **formulate** +13. **formulate** - emits: formulation — record via the matching `amicode_*` tool - Q `formulation`: "The problem shape — trajectory type (gate / state-prep / open-system), fixed-time vs min-time, and any robustness or free-phase? (the infidelity objective is DERIVED from the type; constraints default to the amplitude bound)" — default: a fixed-time gate, free-phase on for entangling gates - [Why?] hooks: free-phase-objective-only, pin-globals-first-solve (read `scores/memory/.md` on request) -15. **solve** +14. **solve** - emits: run, pulse — record via the matching `amicode_*` tool - executor: `local` - vetted template (absolute): `/extension/scores/pulse-designer/templates/solve.jl` - Q `solve_params`: "Pulse duration T (ns), timesteps N, and max_iter?" — default: T = 10 ns, N = 50, max_iter = 60 -16. **inspect** -17. **hardware** (optional) +15. **inspect** +16. **hardware** (optional) - emits: device_session — record via the matching `amicode_*` tool --- @@ -148,40 +146,9 @@ Per-stage guidance and the `amicode_profile` mapping: - If no scannable files are found, say so honestly: "I didn't find any AI-tool configs to import — no worries, we'll build your context as we go." - After seeding (or declining), advance to Stage 5 (demo). + After seeding (or declining), advance to Stage 5. -5. **demo** _(optional)_ — check Julia readiness by calling - `amicode_demo_check`. This returns `{ready: true|false, reason?}`. - - **If ready:** offer the demo: "Want me to run a quick optimization demo so - you can see the workflow end-to-end — the Run Inspector, live iterations, - and a converging result?" Frame it as a WORKFLOW SHOWCASE, not a - domain-specific exercise — it works for all intent selections. - - On accept, call `amicode_demo_launch`. This creates a `__demo__` workspace, - fills the vetted template with stock parameters (T=10ns, N=50, max_iter=60), - and launches through `amico-run --spec`. The Run Inspector streams - iterations live. After FINISHED, report the result: "Solved — F=0.9998 in - 47 iterations" (or whatever the actual numbers are). Then call - `amicode_demo_archive` to clean up the ephemeral workspace. - - **If not ready:** explain honestly: "Julia environment isn't set up yet — - {reason}. No worries, we'll skip the demo. You can always run one later - from the command palette." Advance without blocking. - - **If the user DECLINES the demo:** say "No problem" and advance. - - **If the demo FAILS** (Julia error, convergence failure): report honestly - and continue. A failed demo never blocks onboarding. - - **Constraints:** - - The demo MUST use the vetted template — never free-tier. - - The demo MUST NOT create vault artifacts (no problem card, no pulse bank entry). - - If `isDemoCompleted()` is true (archive marker exists), skip — don't re-offer. - - After the demo (or skipping), advance to Stage 6. - -6. **environment** — _(only if user selected the experiments intent)_ — ask how +5. **environment** — _(only if user selected the experiments intent)_ — ask how experiments will reach hardware. **Pre-fill from seeds:** call `amicode_profile {entity:"status"}` and check if an environment is already recorded from the context-seed (Stage 3). If so, present it as a @@ -192,7 +159,7 @@ Per-stage guidance and the `amicode_profile` mapping: Record: `amicode_profile {entity:"environment", payload:{slug, archetype}}`. Follow up on details per archetype if confirmed. -7. **devices** _(optional, only if user selected the experiments intent)_ — +6. **devices** _(optional, only if user selected the experiments intent)_ — same pre-fill pattern: if a device was seeded, confirm it. Otherwise ask: "Any specific device(s) you want me to remember?" This stage is ALWAYS skippable — "none" or "skip" is a valid answer. @@ -200,13 +167,13 @@ Per-stage guidance and the `amicode_profile` mapping: Record: `amicode_profile {entity:"device", payload:{name, platform, specs}}`. If skipped, move on without recording. -8. **goals** — free-text question via `question` tool with `kind: "text"`: +7. **goals** — free-text question via `question` tool with `kind: "text"`: "What are you hoping to accomplish with Amico?" No pre-fill (goals are personal, not inferrable from configs). Record: `amicode_profile {entity:"profile", payload:{goals:"..."}}`. -9. **handoff** — the terminal stage. FIRST, record the completion marker: +8. **handoff** — the terminal stage. FIRST, record the completion marker: `amicode_profile {entity:"onboarding_completed"}` (exactly once — this is what lets Amico remember them next time and triggers the distiller to materialize the vault). diff --git a/packages/extension/test/scores/overture_rewrite.test.ts b/packages/extension/test/scores/overture_rewrite.test.ts index 765f99a9..196f6c54 100644 --- a/packages/extension/test/scores/overture_rewrite.test.ts +++ b/packages/extension/test/scores/overture_rewrite.test.ts @@ -36,18 +36,19 @@ describe("overture SCORE.md — loads and compiles (AC1)", () => { expect(ov.manifest.schema_version).toBe(1); }); - it("has the new stage structure: orientation, intent, context_seed, demo, environment, devices, goals, handoff", () => { + it("has the new stage structure: orientation, intent, research_area, context_seed, environment, devices, goals, handoff", () => { const ov = overture(); const stageIds = ov.manifest.stages.map((s: { id: string }) => s.id); expect(stageIds).toContain("orientation"); expect(stageIds).toContain("intent"); + expect(stageIds).toContain("research_area"); expect(stageIds).toContain("context_seed"); - expect(stageIds).toContain("demo"); expect(stageIds).toContain("environment"); expect(stageIds).toContain("devices"); expect(stageIds).toContain("goals"); expect(stageIds).toContain("handoff"); - // Old stage name is gone + // Old/removed stages are gone + expect(stageIds).not.toContain("demo"); expect(stageIds).not.toContain("platforms"); expect(stageIds).not.toContain("identity"); }); @@ -148,11 +149,11 @@ describe("overture compiled content — resume (AC8)", () => { describe("overture compiled content — complete flow (AC9)", () => { const md = compileScore(overture()); - it("the overture score is complete: all 8 stages defined end-to-end", () => { + it("the overture score is complete: all stages defined end-to-end", () => { expect(md).toContain("orientation"); expect(md).toContain("intent"); + expect(md).toContain("research_area"); expect(md).toContain("context_seed"); - expect(md).toContain("demo"); expect(md).toContain("environment"); expect(md).toContain("goals"); expect(md).toContain("handoff"); From 3b6be8c196fd3722f3a83cf0166ff3acd65e245b Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Fri, 21 Aug 2026 00:39:17 +0200 Subject: [PATCH 38/43] feat(overture): reorder stages (goals early, context_seed before research_area); auto-generate description at handoff --- packages/extension/scores/overture/SCORE.md | 69 ++++++++++--------- .../test/scores/golden/compile-chained.md | 61 ++++++++-------- .../test/scores/overture_rewrite.test.ts | 9 ++- 3 files changed, 78 insertions(+), 61 deletions(-) diff --git a/packages/extension/scores/overture/SCORE.md b/packages/extension/scores/overture/SCORE.md index 58cf3b07..452ca6a6 100644 --- a/packages/extension/scores/overture/SCORE.md +++ b/packages/extension/scores/overture/SCORE.md @@ -15,6 +15,11 @@ stages: - id: name prompt: "What should I call you?" kind: text + - id: goals + questions: + - id: goals + prompt: "What are you hoping to accomplish with Amico?" + kind: text - id: intent questions: - id: intent @@ -27,12 +32,6 @@ stages: ] multiple: true default: "Perform (automated) experiments and gain scientific insights" - - id: research_area - optional: true - questions: - - id: research_area - prompt: "What research area and what kind of experiments?" - kind: text - id: context_seed optional: true questions: @@ -40,6 +39,12 @@ stages: prompt: "I can scan your existing AI-tool configs to bootstrap your workspace — want me to?" choices: ["Yes, scan my configs", "No thanks, skip"] default: "Yes, scan my configs" + - id: research_area + optional: true + questions: + - id: research_area + prompt: "What research area and what kind of experiments?" + kind: text - id: environment optional: true questions: @@ -59,11 +64,6 @@ stages: - id: devices prompt: "Any specific device(s) you want me to remember? (name, platform, specs — or skip)" default: "skip for now" - - id: goals - questions: - - id: goals - prompt: "What are you hoping to accomplish with Amico?" - kind: text - id: handoff questions: - id: handoff @@ -109,7 +109,13 @@ Per-stage guidance and the `amicode_profile` mapping: Do NOT ask about experience level. Do NOT branch by expertise. The same warm, brief orientation for everyone. -2. **intent** — present a MULTI-SELECT question via the `question` tool with +2. **goals** — free-text question via `question` tool with `kind: "text"`: + "What are you hoping to accomplish with Amico?" No pre-fill (goals are + personal, not inferrable from configs). + + Record: `amicode_profile {entity:"profile", payload:{goals:"..."}}`. + +3. **intent** — present a MULTI-SELECT question via the `question` tool with `multiple: true`. The question: "What brings you to Amicode?" with exactly three options: - "General coding and software development" @@ -123,14 +129,6 @@ Per-stage guidance and the `amicode_profile` mapping: After recording intent, acknowledge briefly ("Got it — let's get you set up") and advance. -3. **research_area** _(optional — only if user selected the experiments intent)_ — - ask via the `question` tool with `kind: "text"`: "What research area and what - kind of experiments?" This is free-form — the user can say anything from - "quantum optimal control for transmon gates" to "protein folding simulations" - to "materials science DFT sweeps." Record whatever they say: - `amicode_profile {entity:"profile", payload:{research_area:"..."}}`. - If the user didn't select the experiments intent, skip this stage entirely. - 4. **context_seed** _(optional)_ — offer an explicit opt-in: "I can scan your existing AI-tool configs (CLAUDE.md, cursor rules, opencode config) to bootstrap your workspace — want me to?" via the `question` tool with the @@ -163,12 +161,20 @@ Per-stage guidance and the `amicode_profile` mapping: - If no scannable files are found, say so honestly: "I didn't find any AI-tool configs to import — no worries, we'll build your context as we go." - After seeding (or declining), advance to Stage 5. + After seeding (or declining), advance. -5. **environment** — _(only if user selected the experiments intent)_ — ask how +5. **research_area** _(optional — only if user selected the experiments intent)_ — + ask via the `question` tool with `kind: "text"`: "What research area and what + kind of experiments?" This is free-form — the user can say anything from + "quantum optimal control for transmon gates" to "protein folding simulations" + to "materials science DFT sweeps." Record whatever they say: + `amicode_profile {entity:"profile", payload:{research_area:"..."}}`. + If the user didn't select the experiments intent, skip this stage entirely. + +6. **environment** — _(only if user selected the experiments intent)_ — ask how experiments will reach hardware. **Pre-fill from seeds:** call `amicode_profile {entity:"status"}` and check if an environment is already - recorded from the context-seed (Stage 3). If so, present it as a + recorded from the context-seed (Stage 4). If so, present it as a confirmation: "I found you use {archetype} — confirm, or change?" via the `question` tool. If no seed, ask the standard choice question with the options above. @@ -176,7 +182,7 @@ Per-stage guidance and the `amicode_profile` mapping: Record: `amicode_profile {entity:"environment", payload:{slug, archetype}}`. Follow up on details per archetype if confirmed. -6. **devices** _(optional, only if user selected the experiments intent)_ — +7. **devices** _(optional, only if user selected the experiments intent)_ — same pre-fill pattern: if a device was seeded, confirm it. Otherwise ask: "Any specific device(s) you want me to remember?" This stage is ALWAYS skippable — "none" or "skip" is a valid answer. @@ -184,18 +190,19 @@ Per-stage guidance and the `amicode_profile` mapping: Record: `amicode_profile {entity:"device", payload:{name, platform, specs}}`. If skipped, move on without recording. -7. **goals** — free-text question via `question` tool with `kind: "text"`: - "What are you hoping to accomplish with Amico?" No pre-fill (goals are - personal, not inferrable from configs). - - Record: `amicode_profile {entity:"profile", payload:{goals:"..."}}`. +8. **handoff** — the terminal stage. FIRST, **auto-generate a description** from + what you've learned (name, goals, research_area, intent, environment) — a + concise 1–2 sentence summary of the user written in third person, suitable + for the "About you" card. Example: "Aaron is a quantum-control researcher + focused on high-fidelity transmon gates, working in simulation." Record: + `amicode_profile {entity:"profile", payload:{description:"..."}}`. -8. **handoff** — the terminal stage. FIRST, record the completion marker: + Then record the completion marker: `amicode_profile {entity:"onboarding_completed"}` (exactly once — this is what lets Amico remember them next time and triggers the distiller to materialize the vault). - Then route by the user's intent selections (from Stage 2 — read from the + Then route by the user's intent selections (from Stage 3 — read from the events stream, do NOT re-ask): - **Research/experiments** selected (alone or combined) → "Let's set up your diff --git a/packages/extension/test/scores/golden/compile-chained.md b/packages/extension/test/scores/golden/compile-chained.md index ad06ded0..dbc2d1a1 100644 --- a/packages/extension/test/scores/golden/compile-chained.md +++ b/packages/extension/test/scores/golden/compile-chained.md @@ -14,18 +14,18 @@ gate's checks pass. 1. **orientation** - Q `name`: "What should I call you?" -2. **intent** +2. **goals** + - Q `goals`: "What are you hoping to accomplish with Amico?" +3. **intent** - Q `intent`: "What brings you to Amicode?" — options: General coding and software development | Perform (automated) experiments and gain scientific insights (recommended) | Exploring -3. **research_area** (optional) - - Q `research_area`: "What research area and what kind of experiments?" 4. **context_seed** (optional) - Q `seed_optin`: "I can scan your existing AI-tool configs to bootstrap your workspace — want me to?" — options: Yes, scan my configs (recommended) | No thanks, skip -5. **environment** (optional) +5. **research_area** (optional) + - Q `research_area`: "What research area and what kind of experiments?" +6. **environment** (optional) - Q `environment`: "How will your experiments reach hardware?" — options: Lab hardware (on-prem control system) | Cloud platform with emulator | Simulation only for now (recommended) | Something else -6. **devices** (optional) +7. **devices** (optional) - Q `devices`: "Any specific device(s) you want me to remember? (name, platform, specs — or skip)" — default: skip for now -7. **goals** - - Q `goals`: "What are you hoping to accomplish with Amico?" 8. **handoff** - Q `handoff`: "Ready to get started?" — options: Let's dive into my first task (recommended) | Open a normal session | Show me around first 9. **platform** @@ -92,7 +92,13 @@ Per-stage guidance and the `amicode_profile` mapping: Do NOT ask about experience level. Do NOT branch by expertise. The same warm, brief orientation for everyone. -2. **intent** — present a MULTI-SELECT question via the `question` tool with +2. **goals** — free-text question via `question` tool with `kind: "text"`: + "What are you hoping to accomplish with Amico?" No pre-fill (goals are + personal, not inferrable from configs). + + Record: `amicode_profile {entity:"profile", payload:{goals:"..."}}`. + +3. **intent** — present a MULTI-SELECT question via the `question` tool with `multiple: true`. The question: "What brings you to Amicode?" with exactly three options: - "General coding and software development" @@ -106,14 +112,6 @@ Per-stage guidance and the `amicode_profile` mapping: After recording intent, acknowledge briefly ("Got it — let's get you set up") and advance. -3. **research_area** _(optional — only if user selected the experiments intent)_ — - ask via the `question` tool with `kind: "text"`: "What research area and what - kind of experiments?" This is free-form — the user can say anything from - "quantum optimal control for transmon gates" to "protein folding simulations" - to "materials science DFT sweeps." Record whatever they say: - `amicode_profile {entity:"profile", payload:{research_area:"..."}}`. - If the user didn't select the experiments intent, skip this stage entirely. - 4. **context_seed** _(optional)_ — offer an explicit opt-in: "I can scan your existing AI-tool configs (CLAUDE.md, cursor rules, opencode config) to bootstrap your workspace — want me to?" via the `question` tool with the @@ -146,12 +144,20 @@ Per-stage guidance and the `amicode_profile` mapping: - If no scannable files are found, say so honestly: "I didn't find any AI-tool configs to import — no worries, we'll build your context as we go." - After seeding (or declining), advance to Stage 5. + After seeding (or declining), advance. -5. **environment** — _(only if user selected the experiments intent)_ — ask how +5. **research_area** _(optional — only if user selected the experiments intent)_ — + ask via the `question` tool with `kind: "text"`: "What research area and what + kind of experiments?" This is free-form — the user can say anything from + "quantum optimal control for transmon gates" to "protein folding simulations" + to "materials science DFT sweeps." Record whatever they say: + `amicode_profile {entity:"profile", payload:{research_area:"..."}}`. + If the user didn't select the experiments intent, skip this stage entirely. + +6. **environment** — _(only if user selected the experiments intent)_ — ask how experiments will reach hardware. **Pre-fill from seeds:** call `amicode_profile {entity:"status"}` and check if an environment is already - recorded from the context-seed (Stage 3). If so, present it as a + recorded from the context-seed (Stage 4). If so, present it as a confirmation: "I found you use {archetype} — confirm, or change?" via the `question` tool. If no seed, ask the standard choice question with the options above. @@ -159,7 +165,7 @@ Per-stage guidance and the `amicode_profile` mapping: Record: `amicode_profile {entity:"environment", payload:{slug, archetype}}`. Follow up on details per archetype if confirmed. -6. **devices** _(optional, only if user selected the experiments intent)_ — +7. **devices** _(optional, only if user selected the experiments intent)_ — same pre-fill pattern: if a device was seeded, confirm it. Otherwise ask: "Any specific device(s) you want me to remember?" This stage is ALWAYS skippable — "none" or "skip" is a valid answer. @@ -167,18 +173,19 @@ Per-stage guidance and the `amicode_profile` mapping: Record: `amicode_profile {entity:"device", payload:{name, platform, specs}}`. If skipped, move on without recording. -7. **goals** — free-text question via `question` tool with `kind: "text"`: - "What are you hoping to accomplish with Amico?" No pre-fill (goals are - personal, not inferrable from configs). - - Record: `amicode_profile {entity:"profile", payload:{goals:"..."}}`. +8. **handoff** — the terminal stage. FIRST, **auto-generate a description** from + what you've learned (name, goals, research_area, intent, environment) — a + concise 1–2 sentence summary of the user written in third person, suitable + for the "About you" card. Example: "Aaron is a quantum-control researcher + focused on high-fidelity transmon gates, working in simulation." Record: + `amicode_profile {entity:"profile", payload:{description:"..."}}`. -8. **handoff** — the terminal stage. FIRST, record the completion marker: + Then record the completion marker: `amicode_profile {entity:"onboarding_completed"}` (exactly once — this is what lets Amico remember them next time and triggers the distiller to materialize the vault). - Then route by the user's intent selections (from Stage 2 — read from the + Then route by the user's intent selections (from Stage 3 — read from the events stream, do NOT re-ask): - **Research/experiments** selected (alone or combined) → "Let's set up your diff --git a/packages/extension/test/scores/overture_rewrite.test.ts b/packages/extension/test/scores/overture_rewrite.test.ts index 196f6c54..a0c88569 100644 --- a/packages/extension/test/scores/overture_rewrite.test.ts +++ b/packages/extension/test/scores/overture_rewrite.test.ts @@ -36,21 +36,24 @@ describe("overture SCORE.md — loads and compiles (AC1)", () => { expect(ov.manifest.schema_version).toBe(1); }); - it("has the new stage structure: orientation, intent, research_area, context_seed, environment, devices, goals, handoff", () => { + it("has the new stage structure: orientation, goals, intent, context_seed, research_area, environment, devices, handoff", () => { const ov = overture(); const stageIds = ov.manifest.stages.map((s: { id: string }) => s.id); expect(stageIds).toContain("orientation"); + expect(stageIds).toContain("goals"); expect(stageIds).toContain("intent"); - expect(stageIds).toContain("research_area"); expect(stageIds).toContain("context_seed"); + expect(stageIds).toContain("research_area"); expect(stageIds).toContain("environment"); expect(stageIds).toContain("devices"); - expect(stageIds).toContain("goals"); expect(stageIds).toContain("handoff"); // Old/removed stages are gone expect(stageIds).not.toContain("demo"); expect(stageIds).not.toContain("platforms"); expect(stageIds).not.toContain("identity"); + // Verify order: goals before intent, context_seed before research_area + expect(stageIds.indexOf("goals")).toBeLessThan(stageIds.indexOf("intent")); + expect(stageIds.indexOf("context_seed")).toBeLessThan(stageIds.indexOf("research_area")); }); it("compiles to markdown without error (standalone)", () => { From 77fdf4397fe494fa4e924bdebbbeb6eedd063d95 Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Fri, 21 Aug 2026 00:42:01 +0200 Subject: [PATCH 39/43] =?UTF-8?q?fix(overture):=20reorder=20=E2=80=94=20co?= =?UTF-8?q?ntext=5Fseed=20after=20orientation,=20intent=20before=20goals?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/extension/scores/overture/SCORE.md | 64 +++++++++---------- .../test/scores/golden/compile-chained.md | 52 +++++++-------- .../test/scores/overture_rewrite.test.ts | 12 ++-- 3 files changed, 64 insertions(+), 64 deletions(-) diff --git a/packages/extension/scores/overture/SCORE.md b/packages/extension/scores/overture/SCORE.md index 452ca6a6..6e0f89f8 100644 --- a/packages/extension/scores/overture/SCORE.md +++ b/packages/extension/scores/overture/SCORE.md @@ -15,11 +15,13 @@ stages: - id: name prompt: "What should I call you?" kind: text - - id: goals + - id: context_seed + optional: true questions: - - id: goals - prompt: "What are you hoping to accomplish with Amico?" - kind: text + - id: seed_optin + prompt: "I can scan your existing AI-tool configs to bootstrap your workspace — want me to?" + choices: ["Yes, scan my configs", "No thanks, skip"] + default: "Yes, scan my configs" - id: intent questions: - id: intent @@ -32,13 +34,11 @@ stages: ] multiple: true default: "Perform (automated) experiments and gain scientific insights" - - id: context_seed - optional: true + - id: goals questions: - - id: seed_optin - prompt: "I can scan your existing AI-tool configs to bootstrap your workspace — want me to?" - choices: ["Yes, scan my configs", "No thanks, skip"] - default: "Yes, scan my configs" + - id: goals + prompt: "What are you hoping to accomplish with Amico?" + kind: text - id: research_area optional: true questions: @@ -109,27 +109,7 @@ Per-stage guidance and the `amicode_profile` mapping: Do NOT ask about experience level. Do NOT branch by expertise. The same warm, brief orientation for everyone. -2. **goals** — free-text question via `question` tool with `kind: "text"`: - "What are you hoping to accomplish with Amico?" No pre-fill (goals are - personal, not inferrable from configs). - - Record: `amicode_profile {entity:"profile", payload:{goals:"..."}}`. - -3. **intent** — present a MULTI-SELECT question via the `question` tool with - `multiple: true`. The question: "What brings you to Amicode?" with exactly - three options: - - "General coding and software development" - - "Perform (automated) experiments and gain scientific insights" - - "Exploring" - - The user may select any combination (1, 2, or all 3). Record: - `amicode_profile {entity:"profile", payload:{intent:["research","general_coding","exploring"]}}`. - Use lowercase slug forms in the array: `research`, `general_coding`, `exploring`. - - After recording intent, acknowledge briefly ("Got it — let's get you set up") - and advance. - -4. **context_seed** _(optional)_ — offer an explicit opt-in: "I can scan your +2. **context_seed** _(optional)_ — offer an explicit opt-in: "I can scan your existing AI-tool configs (CLAUDE.md, cursor rules, opencode config) to bootstrap your workspace — want me to?" via the `question` tool with the two choices above. @@ -163,6 +143,26 @@ Per-stage guidance and the `amicode_profile` mapping: After seeding (or declining), advance. +3. **intent** — present a MULTI-SELECT question via the `question` tool with + `multiple: true`. The question: "What brings you to Amicode?" with exactly + three options: + - "General coding and software development" + - "Perform (automated) experiments and gain scientific insights" + - "Exploring" + + The user may select any combination (1, 2, or all 3). Record: + `amicode_profile {entity:"profile", payload:{intent:["research","general_coding","exploring"]}}`. + Use lowercase slug forms in the array: `research`, `general_coding`, `exploring`. + + After recording intent, acknowledge briefly ("Got it — let's get you set up") + and advance. + +4. **goals** — free-text question via `question` tool with `kind: "text"`: + "What are you hoping to accomplish with Amico?" No pre-fill (goals are + personal, not inferrable from configs). + + Record: `amicode_profile {entity:"profile", payload:{goals:"..."}}`. + 5. **research_area** _(optional — only if user selected the experiments intent)_ — ask via the `question` tool with `kind: "text"`: "What research area and what kind of experiments?" This is free-form — the user can say anything from @@ -174,7 +174,7 @@ Per-stage guidance and the `amicode_profile` mapping: 6. **environment** — _(only if user selected the experiments intent)_ — ask how experiments will reach hardware. **Pre-fill from seeds:** call `amicode_profile {entity:"status"}` and check if an environment is already - recorded from the context-seed (Stage 4). If so, present it as a + recorded from the context-seed (Stage 2). If so, present it as a confirmation: "I found you use {archetype} — confirm, or change?" via the `question` tool. If no seed, ask the standard choice question with the options above. diff --git a/packages/extension/test/scores/golden/compile-chained.md b/packages/extension/test/scores/golden/compile-chained.md index dbc2d1a1..12499850 100644 --- a/packages/extension/test/scores/golden/compile-chained.md +++ b/packages/extension/test/scores/golden/compile-chained.md @@ -14,12 +14,12 @@ gate's checks pass. 1. **orientation** - Q `name`: "What should I call you?" -2. **goals** - - Q `goals`: "What are you hoping to accomplish with Amico?" +2. **context_seed** (optional) + - Q `seed_optin`: "I can scan your existing AI-tool configs to bootstrap your workspace — want me to?" — options: Yes, scan my configs (recommended) | No thanks, skip 3. **intent** - Q `intent`: "What brings you to Amicode?" — options: General coding and software development | Perform (automated) experiments and gain scientific insights (recommended) | Exploring -4. **context_seed** (optional) - - Q `seed_optin`: "I can scan your existing AI-tool configs to bootstrap your workspace — want me to?" — options: Yes, scan my configs (recommended) | No thanks, skip +4. **goals** + - Q `goals`: "What are you hoping to accomplish with Amico?" 5. **research_area** (optional) - Q `research_area`: "What research area and what kind of experiments?" 6. **environment** (optional) @@ -92,27 +92,7 @@ Per-stage guidance and the `amicode_profile` mapping: Do NOT ask about experience level. Do NOT branch by expertise. The same warm, brief orientation for everyone. -2. **goals** — free-text question via `question` tool with `kind: "text"`: - "What are you hoping to accomplish with Amico?" No pre-fill (goals are - personal, not inferrable from configs). - - Record: `amicode_profile {entity:"profile", payload:{goals:"..."}}`. - -3. **intent** — present a MULTI-SELECT question via the `question` tool with - `multiple: true`. The question: "What brings you to Amicode?" with exactly - three options: - - "General coding and software development" - - "Perform (automated) experiments and gain scientific insights" - - "Exploring" - - The user may select any combination (1, 2, or all 3). Record: - `amicode_profile {entity:"profile", payload:{intent:["research","general_coding","exploring"]}}`. - Use lowercase slug forms in the array: `research`, `general_coding`, `exploring`. - - After recording intent, acknowledge briefly ("Got it — let's get you set up") - and advance. - -4. **context_seed** _(optional)_ — offer an explicit opt-in: "I can scan your +2. **context_seed** _(optional)_ — offer an explicit opt-in: "I can scan your existing AI-tool configs (CLAUDE.md, cursor rules, opencode config) to bootstrap your workspace — want me to?" via the `question` tool with the two choices above. @@ -146,6 +126,26 @@ Per-stage guidance and the `amicode_profile` mapping: After seeding (or declining), advance. +3. **intent** — present a MULTI-SELECT question via the `question` tool with + `multiple: true`. The question: "What brings you to Amicode?" with exactly + three options: + - "General coding and software development" + - "Perform (automated) experiments and gain scientific insights" + - "Exploring" + + The user may select any combination (1, 2, or all 3). Record: + `amicode_profile {entity:"profile", payload:{intent:["research","general_coding","exploring"]}}`. + Use lowercase slug forms in the array: `research`, `general_coding`, `exploring`. + + After recording intent, acknowledge briefly ("Got it — let's get you set up") + and advance. + +4. **goals** — free-text question via `question` tool with `kind: "text"`: + "What are you hoping to accomplish with Amico?" No pre-fill (goals are + personal, not inferrable from configs). + + Record: `amicode_profile {entity:"profile", payload:{goals:"..."}}`. + 5. **research_area** _(optional — only if user selected the experiments intent)_ — ask via the `question` tool with `kind: "text"`: "What research area and what kind of experiments?" This is free-form — the user can say anything from @@ -157,7 +157,7 @@ Per-stage guidance and the `amicode_profile` mapping: 6. **environment** — _(only if user selected the experiments intent)_ — ask how experiments will reach hardware. **Pre-fill from seeds:** call `amicode_profile {entity:"status"}` and check if an environment is already - recorded from the context-seed (Stage 4). If so, present it as a + recorded from the context-seed (Stage 2). If so, present it as a confirmation: "I found you use {archetype} — confirm, or change?" via the `question` tool. If no seed, ask the standard choice question with the options above. diff --git a/packages/extension/test/scores/overture_rewrite.test.ts b/packages/extension/test/scores/overture_rewrite.test.ts index a0c88569..0752f507 100644 --- a/packages/extension/test/scores/overture_rewrite.test.ts +++ b/packages/extension/test/scores/overture_rewrite.test.ts @@ -36,13 +36,13 @@ describe("overture SCORE.md — loads and compiles (AC1)", () => { expect(ov.manifest.schema_version).toBe(1); }); - it("has the new stage structure: orientation, goals, intent, context_seed, research_area, environment, devices, handoff", () => { + it("has the new stage structure: orientation, context_seed, intent, goals, research_area, environment, devices, handoff", () => { const ov = overture(); const stageIds = ov.manifest.stages.map((s: { id: string }) => s.id); expect(stageIds).toContain("orientation"); - expect(stageIds).toContain("goals"); - expect(stageIds).toContain("intent"); expect(stageIds).toContain("context_seed"); + expect(stageIds).toContain("intent"); + expect(stageIds).toContain("goals"); expect(stageIds).toContain("research_area"); expect(stageIds).toContain("environment"); expect(stageIds).toContain("devices"); @@ -51,9 +51,9 @@ describe("overture SCORE.md — loads and compiles (AC1)", () => { expect(stageIds).not.toContain("demo"); expect(stageIds).not.toContain("platforms"); expect(stageIds).not.toContain("identity"); - // Verify order: goals before intent, context_seed before research_area - expect(stageIds.indexOf("goals")).toBeLessThan(stageIds.indexOf("intent")); - expect(stageIds.indexOf("context_seed")).toBeLessThan(stageIds.indexOf("research_area")); + // Verify order: context_seed before intent, intent before goals + expect(stageIds.indexOf("context_seed")).toBeLessThan(stageIds.indexOf("intent")); + expect(stageIds.indexOf("intent")).toBeLessThan(stageIds.indexOf("goals")); }); it("compiles to markdown without error (standalone)", () => { From eed2c78c6681a5f162e04c6fa95ef19fb9dbb830 Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Fri, 21 Aug 2026 00:55:33 +0200 Subject: [PATCH 40/43] fix(overture): handoff opens normal session for all intents (no pulse-designer auto-chain) --- packages/extension/scores/overture/SCORE.md | 18 ++++-------------- .../test/scores/golden/compile-chained.md | 18 ++++-------------- .../test/scores/overture_rewrite.test.ts | 2 +- 3 files changed, 9 insertions(+), 29 deletions(-) diff --git a/packages/extension/scores/overture/SCORE.md b/packages/extension/scores/overture/SCORE.md index 6e0f89f8..fcb4cc62 100644 --- a/packages/extension/scores/overture/SCORE.md +++ b/packages/extension/scores/overture/SCORE.md @@ -202,19 +202,9 @@ Per-stage guidance and the `amicode_profile` mapping: what lets Amico remember them next time and triggers the distiller to materialize the vault). - Then route by the user's intent selections (from Stage 3 — read from the - events stream, do NOT re-ask): - - - **Research/experiments** selected (alone or combined) → "Let's set up your - first experiment" → continue straight into the **pulse-designer interview** - in this same session. Use everything learned (environment, device) to - skip questions already answered. - - **Research + General coding** → same as above, but acknowledge: "I'm also - your general coding companion — you can switch modes any time." - - **General coding only** (no Research) → open a normal session: "You're all - set — I'll remember your context across sessions. Ask me anything." - Highlight memory + vault features briefly. - - **Exploring only** → "Welcome aboard — want a quick tour of what I can do, - or just dive in?" Offer a brief orientation tour. + Then open a normal session for all users: "You're all set — I'll remember + your context across sessions. Ask me anything." Briefly highlight that Amico + remembers context, adapts over time, and can help with coding or experiments + depending on what they selected. The handoff does NOT re-ask intent — it reads what was recorded and routes. diff --git a/packages/extension/test/scores/golden/compile-chained.md b/packages/extension/test/scores/golden/compile-chained.md index 12499850..26709cb2 100644 --- a/packages/extension/test/scores/golden/compile-chained.md +++ b/packages/extension/test/scores/golden/compile-chained.md @@ -185,20 +185,10 @@ Per-stage guidance and the `amicode_profile` mapping: what lets Amico remember them next time and triggers the distiller to materialize the vault). - Then route by the user's intent selections (from Stage 3 — read from the - events stream, do NOT re-ask): - - - **Research/experiments** selected (alone or combined) → "Let's set up your - first experiment" → continue straight into the **pulse-designer interview** - in this same session. Use everything learned (environment, device) to - skip questions already answered. - - **Research + General coding** → same as above, but acknowledge: "I'm also - your general coding companion — you can switch modes any time." - - **General coding only** (no Research) → open a normal session: "You're all - set — I'll remember your context across sessions. Ask me anything." - Highlight memory + vault features briefly. - - **Exploring only** → "Welcome aboard — want a quick tour of what I can do, - or just dive in?" Offer a brief orientation tour. + Then open a normal session for all users: "You're all set — I'll remember + your context across sessions. Ask me anything." Briefly highlight that Amico + remembers context, adapts over time, and can help with coding or experiments + depending on what they selected. The handoff does NOT re-ask intent — it reads what was recorded and routes. diff --git a/packages/extension/test/scores/overture_rewrite.test.ts b/packages/extension/test/scores/overture_rewrite.test.ts index 0752f507..722e3d6a 100644 --- a/packages/extension/test/scores/overture_rewrite.test.ts +++ b/packages/extension/test/scores/overture_rewrite.test.ts @@ -101,7 +101,7 @@ describe("overture compiled content — Stage 2 intent (AC4, AC5, AC6)", () => { it("AC4: presents exactly three options for multi-select", () => { expect(md).toContain("General coding and software development"); - expect(md).toContain("Research"); + expect(md).toContain("Perform (automated) experiments and gain scientific insights"); expect(md).toContain("Exploring"); }); From dbbf59f48dada564278848454ceac9d3f8e056e6 Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Fri, 21 Aug 2026 01:07:55 +0200 Subject: [PATCH 41/43] =?UTF-8?q?fix(overture):=20handoff=20ends=20cleanly?= =?UTF-8?q?=20=E2=80=94=20update=20About=20You,=20tell=20user=20to=20reloa?= =?UTF-8?q?d,=20no=20auto-chaining?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/extension/scores/overture/SCORE.md | 11 ++++++----- .../extension/test/scores/golden/compile-chained.md | 11 ++++++----- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/extension/scores/overture/SCORE.md b/packages/extension/scores/overture/SCORE.md index fcb4cc62..1de27c87 100644 --- a/packages/extension/scores/overture/SCORE.md +++ b/packages/extension/scores/overture/SCORE.md @@ -202,9 +202,10 @@ Per-stage guidance and the `amicode_profile` mapping: what lets Amico remember them next time and triggers the distiller to materialize the vault). - Then open a normal session for all users: "You're all set — I'll remember - your context across sessions. Ask me anything." Briefly highlight that Amico - remembers context, adapts over time, and can help with coding or experiments - depending on what they selected. + Then tell the user onboarding is complete: "You're all set — your About You + card on the dashboard is now populated with what you told me. To see it, + reload the window (Cmd+Shift+P → 'Reload Window', or Cmd+R). After that, + start a new session anytime to explore what Amico can do." - The handoff does NOT re-ask intent — it reads what was recorded and routes. + Do NOT auto-chain into another interview or open a new session. The + onboarding ends here. The user is in control of what happens next. diff --git a/packages/extension/test/scores/golden/compile-chained.md b/packages/extension/test/scores/golden/compile-chained.md index 26709cb2..397d7057 100644 --- a/packages/extension/test/scores/golden/compile-chained.md +++ b/packages/extension/test/scores/golden/compile-chained.md @@ -185,12 +185,13 @@ Per-stage guidance and the `amicode_profile` mapping: what lets Amico remember them next time and triggers the distiller to materialize the vault). - Then open a normal session for all users: "You're all set — I'll remember - your context across sessions. Ask me anything." Briefly highlight that Amico - remembers context, adapts over time, and can help with coding or experiments - depending on what they selected. + Then tell the user onboarding is complete: "You're all set — your About You + card on the dashboard is now populated with what you told me. To see it, + reload the window (Cmd+Shift+P → 'Reload Window', or Cmd+R). After that, + start a new session anytime to explore what Amico can do." - The handoff does NOT re-ask intent — it reads what was recorded and routes. + Do NOT auto-chain into another interview or open a new session. The + onboarding ends here. The user is in control of what happens next. --- From ca0951da5e87bdf18d7f7903b4317c44233b7a6f Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Sat, 22 Aug 2026 17:42:53 -0400 Subject: [PATCH 42/43] fix(onboarding): don't write garbage model ID for unknown providers When the active provider (e.g. amazon-bedrock) has no entry in PROVIDER_MODELS, writeBatchConfig was writing 'provider/unknown' as the model field. This caused HTTP 500 on the server when trying to resolve it (e.g. bug reporter arm failing). - writeBatchConfig: omit model field when no known default exists - writeOnboardingConfig: skip model if it ends with '/unknown' or is empty - Both functions now let the server resolve its own default from the connected provider's model list --- packages/extension/src/credential_scanner.ts | 15 +++++++++++---- packages/extension/src/onboarding_panel.ts | 19 ++++++++++++++----- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/packages/extension/src/credential_scanner.ts b/packages/extension/src/credential_scanner.ts index c018bc4e..c4589a76 100644 --- a/packages/extension/src/credential_scanner.ts +++ b/packages/extension/src/credential_scanner.ts @@ -335,16 +335,23 @@ export function writeBatchConfig( providerEntry[cred.provider] = entry; } - // Determine active model + // Determine active model — only set when we have a known default. + // Unknown providers (e.g. amazon-bedrock) let the server resolve its own + // default from the connected provider's model list. const activeModels = PROVIDER_MODELS[activeProvider]; - const activeModel = activeModels?.[0]?.id ?? `${activeProvider}/unknown`; + const activeModel = activeModels?.[0]?.id; - const result = { + const result: Record = { ...existing, $schema: "https://opencode.ai/config.json", provider: providerEntry, - model: activeModel, }; + if (activeModel) { + result.model = activeModel; + } else { + // Remove stale model field if it points to an unknown model + delete result.model; + } fs.writeFileSync(targetPath, JSON.stringify(result, null, 2) + "\n"); } diff --git a/packages/extension/src/onboarding_panel.ts b/packages/extension/src/onboarding_panel.ts index 653c01ac..faabf5d7 100644 --- a/packages/extension/src/onboarding_panel.ts +++ b/packages/extension/src/onboarding_panel.ts @@ -114,15 +114,18 @@ export function writeOnboardingConfig( // If parsing fails, start fresh } - // Reject placeholder/invalid keys (#455) — but allow empty keys (OAuth providers) + // Reject placeholder/invalid keys (#455) — but allow empty keys (OAuth providers) if (config.apiKey && !isValidApiKey(config.apiKey)) { // Key is non-empty but invalid — don't write this provider, just preserve existing config - const result = { + const result: Record = { ...existing, $schema: "https://opencode.ai/config.json", provider: existing.provider ?? {}, - model: config.model, }; + // Only write model if it's a known valid ID (not empty, not "provider/unknown") + if (config.model && !config.model.endsWith("/unknown")) { + result.model = config.model; + } fs.writeFileSync(configPath, JSON.stringify(result, null, 2) + "\n"); return; } @@ -146,12 +149,18 @@ export function writeOnboardingConfig( [config.provider]: providerConfig, }; - const result = { + const result: Record = { ...existing, $schema: "https://opencode.ai/config.json", provider: providerEntry, - model: config.model, }; + // Only write model if it's a known valid ID (not empty, not "provider/unknown") + if (config.model && !config.model.endsWith("/unknown")) { + result.model = config.model; + } else { + // Remove stale model field that points to an unknown model + delete result.model; + } fs.writeFileSync(configPath, JSON.stringify(result, null, 2) + "\n"); } From b52ea01a7455ff4b1028bfe610c481d13e2f5ab5 Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Sat, 22 Aug 2026 17:53:15 -0400 Subject: [PATCH 43/43] fix(bug-reporter): don't pass stale model pin to arm request MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bug reporter was passing amicode.defaultModel (e.g. 'anthropic/claude-sonnet-4') to the /session/:id/command endpoint. When only amazon-bedrock is connected, the server can't route to the anthropic provider and returns 500. Fix: - bug_report.ts: never pass a model to armSession — let the server resolve its own default from the first connected provider - opencode_config.ts: add validatedModelPin() that checks the model's provider exists in the user's configured providers before injecting into the project config - extension.ts: all 4 model-pin injection sites now use validatedModelPin() - onboarding_panel.ts: clear amicode.defaultModel on both config-success and confirm-import (the old provider's model is stale by definition) --- packages/extension/src/bug_report.ts | 12 ++++------ packages/extension/src/extension.ts | 11 +++++---- packages/extension/src/onboarding_panel.ts | 5 ++++ packages/extension/src/opencode_config.ts | 23 +++++++++++++++++++ packages/extension/test/bug_report.test.ts | 2 +- .../test/scores/golden/router-section.md | 2 +- 6 files changed, 42 insertions(+), 13 deletions(-) diff --git a/packages/extension/src/bug_report.ts b/packages/extension/src/bug_report.ts index 3cf84531..963362b5 100644 --- a/packages/extension/src/bug_report.ts +++ b/packages/extension/src/bug_report.ts @@ -299,16 +299,14 @@ export class BugReportManager { /** Arm: the report-a-bug slash command as the session's first turn. * - * `model` is optional on POST /session/:id/command (a `provider/model` - * string; the route also takes `variant`). We send it only when - * `amicode.defaultModel` is explicitly set — otherwise the field is omitted - * entirely and the server resolves its own default, which is the documented - * behaviour for an unpinned install. */ + * The model field is always omitted — the server resolves its own default + * from the first connected provider's best model. Passing a stale + * `amicode.defaultModel` that references an unconnected provider causes a + * 500 (the server can't route to a disconnected provider). */ private async armSession(server: BugReportServer, sessionID: string): Promise { - const model = this.deps.defaultModel?.()?.trim(); const res = await this.fetch(new URL(`/session/${sessionID}/command`, server.url), server, { method: "POST", - body: { command: REPORT_A_BUG_SKILL, arguments: "", ...(model ? { model } : {}) }, + body: { command: REPORT_A_BUG_SKILL, arguments: "" }, }); if (!res.ok) throw new Error(`couldn't arm the report-a-bug skill (HTTP ${res.status})`); } diff --git a/packages/extension/src/extension.ts b/packages/extension/src/extension.ts index a8498f87..b46b3e69 100644 --- a/packages/extension/src/extension.ts +++ b/packages/extension/src/extension.ts @@ -13,6 +13,7 @@ import { resolveJuliaProject, buildOpencodeConfigContent, resolveModelPin, + validatedModelPin, } from "./opencode_config"; import { parseLibraryRootSpecs } from "./scores/package_skills"; import { resolveAmicoRunBinDir, resolveRunsRoot } from "./opencode_paths"; @@ -709,7 +710,9 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { // the user's recent selection, else the provider default. A hardcoded // fallback here used to override the user's own choice. The in-chat // picker still overrides per session. - vscode.workspace.getConfiguration("amicode").get("defaultModel", "").trim() || resolveModelPin(), + // Validate: don't inject a pin that references an unconnected provider — + // it causes 500s when the server tries to resolve it. + validatedModelPin(vscode.workspace.getConfiguration("amicode").get("defaultModel", "").trim() || resolveModelPin()), // Telemetry gate → experimental.openTelemetry (span generation), coupled // to the exporter env this same spawnEnv resolves. telemetryOpen(), @@ -777,7 +780,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { // Armonia mount stack (spec-20260707-002846 C1): per-mount read grants. project2.mounts, // Same pin rule as boot: only an explicit amicode.defaultModel pins. - vscode.workspace.getConfiguration("amicode").get("defaultModel", "").trim() || resolveModelPin(), + validatedModelPin(vscode.workspace.getConfiguration("amicode").get("defaultModel", "").trim() || resolveModelPin()), telemetryOpen(), // gate → experimental.openTelemetry (span generation) // Context plugin: injects live stack state per system-prompt build. [path.resolve(ctx.extensionPath, "opencode-plugin", "amicode_context.ts")], @@ -936,7 +939,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { project2.skillsStageDir, project2.vaultDir, project2.mounts, - vscode.workspace.getConfiguration("amicode").get("defaultModel", "").trim() || resolveModelPin(), + validatedModelPin(vscode.workspace.getConfiguration("amicode").get("defaultModel", "").trim() || resolveModelPin()), telemetryOpen(), // gate → experimental.openTelemetry (span generation) ), }), @@ -1353,7 +1356,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { opencodeProject.skillsStageDir, opencodeProject.vaultDir, opencodeProject.mounts, - vscode.workspace.getConfiguration("amicode").get("defaultModel", "").trim() || resolveModelPin(), + validatedModelPin(vscode.workspace.getConfiguration("amicode").get("defaultModel", "").trim() || resolveModelPin()), telemetryOpen(), ), }), diff --git a/packages/extension/src/onboarding_panel.ts b/packages/extension/src/onboarding_panel.ts index faabf5d7..1dbb9e3e 100644 --- a/packages/extension/src/onboarding_panel.ts +++ b/packages/extension/src/onboarding_panel.ts @@ -462,6 +462,9 @@ export function registerOnboardingPanel(ctx: vscode.ExtensionContext): void { } else if (msg.type === "config-success") { const payload = msg.payload as OnboardingConfig; writeOnboardingConfig(payload); + // Clear stale model pin — the old provider may no longer be connected. + // The server will resolve the new provider's default on its own. + void vscode.workspace.getConfiguration("amicode").update("defaultModel", undefined, vscode.ConfigurationTarget.Global); // Swap the panel HTML directly to the splash (same as confirm-import) panel.webview.html = splashHtml(); // Signal that the next chat panel open should auto-send the onboarding greeting @@ -552,6 +555,8 @@ export function registerOnboardingPanel(ctx: vscode.ExtensionContext): void { } heldCredentials = []; testResults.clear(); + // Clear stale model pin — the old provider may no longer be connected. + void vscode.workspace.getConfiguration("amicode").update("defaultModel", undefined, vscode.ConfigurationTarget.Global); // Swap the panel HTML directly to the splash — no webview-side // DOM manipulation, so there's no flash when adopt() fires later // (adopt's overlay uses the exact same SVG + CSS). diff --git a/packages/extension/src/opencode_config.ts b/packages/extension/src/opencode_config.ts index 71e35b14..3e60b4a9 100644 --- a/packages/extension/src/opencode_config.ts +++ b/packages/extension/src/opencode_config.ts @@ -381,6 +381,29 @@ export function resolveModelPin(): string | undefined { return undefined; } +/** Validate a model pin against the user's configured providers. + * Returns the pin unchanged if its provider is configured, otherwise undefined. + * This prevents injecting a stale pin that references a disconnected provider + * (which causes 500s when the server tries to resolve it). */ +export function validatedModelPin(pin: string | undefined): string | undefined { + if (!pin) return undefined; + const providerID = pin.split("/")[0]; + if (!providerID) return undefined; + // Read the user's global opencode.json to check configured providers + const configPath = path.join(os.homedir(), ".config", "opencode", "opencode.json"); + try { + if (!fs.existsSync(configPath)) return pin; // no config → trust the pin (first boot) + const raw = JSON.parse(fs.readFileSync(configPath, "utf8")); + const providers = Object.keys(raw?.provider ?? {}); + if (providers.length === 0) return pin; // no providers section → trust the pin + // The pin's provider must be in the configured set + if (providers.includes(providerID)) return pin; + return undefined; // provider not configured — don't inject stale pin + } catch { + return pin; // can't read config → trust the pin + } +} + export function buildOpencodeConfigContent( agentsPath: string, templatePath: string, diff --git a/packages/extension/test/bug_report.test.ts b/packages/extension/test/bug_report.test.ts index 82e0d1f8..53c33568 100644 --- a/packages/extension/test/bug_report.test.ts +++ b/packages/extension/test/bug_report.test.ts @@ -89,7 +89,7 @@ describe("amicode.reportBug — create, arm, open (AC1)", () => { }); const arm = calls.filter((c) => c.url.endsWith("/session/ses_bug1/command")); expect(arm).toHaveLength(1); - expect(arm[0].body).toEqual({ command: "report-a-bug", arguments: "", model: "opencode/deepseek-v4-pro" }); + expect(arm[0].body).toEqual({ command: "report-a-bug", arguments: "" }); expect(posted).toEqual([{ source: "amicode", kind: "open-bug-report", sessionID: "ses_bug1" }]); }); diff --git a/packages/extension/test/scores/golden/router-section.md b/packages/extension/test/scores/golden/router-section.md index d284234f..5dc716fc 100644 --- a/packages/extension/test/scores/golden/router-section.md +++ b/packages/extension/test/scores/golden/router-section.md @@ -28,4 +28,4 @@ fleet option with the application entry cards: Never a dead end: if nothing usable is found for an option, say so and offer the others. If candidates match multiple paths equally, ask — never route by silent heuristic. A user who opens with a specific ask ("X gate, 10 ns, -defaults") skips the question entirely and gets straight to it. +defaults") skips the question entirely and gets straight to it. \ No newline at end of file