Symptom
With 0.2.1, the Report a Bug composer button renders and is clickable, but clicking it never opens a working bug dock — from the user's side, the button "won't send messages." Observed 2026-08-08 on a fleet install where the canonical opencode server runs outside the extension host (launchd, amicode-server.sh).
Root cause
The button and the arm step gate on different skill sets:
bugReportSkillStaged() (and the app's amicode_bug_report boot param) evaluate the extension's skill set — the VSIX ships skills/report-a-bug, so the button renders.
BugReportManager.armSession() POSTs /session/:id/command with command: "report-a-bug" against the server's staged skills. When the server is the launchd canonical server, that set is the wrapper's static staging snapshot — which predated report-a-bug, so the arm 404s.
What follows is per spec but reads terribly: open() catches, deletes the orphaned session, and shows an error toast. The composer, however, appears to stay in bug mode wired to the now-deleted session — subsequent sends go nowhere. (App-side recovery gap; the composer should exit bug mode when the open fails, or never enter it until open-bug-report lands.)
Evidence: server GET /command listed 41 commands, none named report-a-bug; the chat DB contained zero sessions with bug_report metadata despite multiple button clicks; the fix was staging the skill server-side.
Suggested fixes
- Pre-flight the arm: the extension can GET
/command (or the boot-time equivalent) and, when report-a-bug is absent server-side, fail with an actionable error — "the opencode server's staged skills are stale (missing report-a-bug); restart/re-stage the server" — instead of a generic arm failure.
- App-side recovery: on a failed/never-completed open, the composer exits bug mode (or never enters it); a failed click must not mute the normal composer.
- Docs: note in ADR-0004's lifecycle that the arm target is server-side state, so any non-extension-hosted server must re-stage skills on extension updates. (Fleet-side, the server wrapper now rsyncs the newest VSIX's
skills/ into its staging dir at boot — additive, no deletes — which closes the drift class for that deployment.)
Refs: #251 (bug-session orchestration), ADR-0004, #295 (related server-misconfiguration guard, draft).
Implementation Plan
Layer 1: Pre-flight the arm (extension-side)
File: packages/extension/src/bug_report.ts
In open(), before createSession(), GET /command from the server and verify report-a-bug is in the response. If absent, show an actionable error and return without creating a session.
New private method:
private async serverHasCommand(server: BugReportServer, name: string): Promise<boolean> {
try {
const res = await this.fetch(new URL("/command", server.url), server, { method: "GET" });
if (!res.ok) return true; // optimistic on probe failure — fall through to arm
const commands: { name: string }[] = await res.json();
return commands.some((c) => c.name === name);
} catch {
return true; // network failure: let the arm path handle it
}
}
Call site (in open(), before createSession):
if (!(await this.serverHasCommand(server, REPORT_A_BUG_SKILL))) {
this.deps.showError(
"Amicode: the opencode server's staged skills are stale (missing report-a-bug). " +
"Restart or re-stage the server.",
);
return;
}
Fail-open: if the GET itself fails (network, server busy), return true and let the existing arm-failure path handle it. Never block the happy path on a probe failure.
Layer 2: Reactive liveness effect (app-side)
File: packages/app/src/pages/session/composer/session-bug-dock.tsx
Add a createEffect that watches the session map when the dock is in "chat" phase. If the dock's current sessionID vanishes from the synced session-info (because the extension deleted it), auto-dismiss silently via controller.dismiss(). No new HTTP calls — rides the existing session-info subscription. No toast (the extension already showed one).
// Liveness watch: if the dock's session vanishes externally (arm-failure
// cleanup race, or any unexpected deletion), auto-dismiss silently.
createEffect(() => {
if (bugDockController.phase() !== "chat") return;
const id = bugDockController.sessionID();
if (!id) return;
const info = serverSync().session.data.info ?? {};
if (!(id in info)) {
bugDockController.dismiss();
}
});
Layer 3: ADR-0004 update
Append to "Accepted costs": the arm target (/session/:id/command) resolves against the server's command set, not the extension's staged skill paths. A non-extension-hosted server must re-stage skills on extension updates; the extension pre-flights the arm and fails actionably when the skill is absent server-side. The fleet wrapper's additive rsync-at-boot closes this drift class.
Acceptance Criteria
Scope
- Extension:
bug_report.ts (~15 lines new)
- App:
session-bug-dock.tsx (~8 lines new)
- Docs:
docs/adr/0004-bug-session-lifecycle.md (~3 lines appended)
- Tests:
test/bug_report.test.ts (1 new describe block), app dock test (1 new case)
Symptom
With 0.2.1, the Report a Bug composer button renders and is clickable, but clicking it never opens a working bug dock — from the user's side, the button "won't send messages." Observed 2026-08-08 on a fleet install where the canonical opencode server runs outside the extension host (launchd,
amicode-server.sh).Root cause
The button and the arm step gate on different skill sets:
bugReportSkillStaged()(and the app'samicode_bug_reportboot param) evaluate the extension's skill set — the VSIX shipsskills/report-a-bug, so the button renders.BugReportManager.armSession()POSTs/session/:id/commandwithcommand: "report-a-bug"against the server's staged skills. When the server is the launchd canonical server, that set is the wrapper's static staging snapshot — which predatedreport-a-bug, so the arm 404s.What follows is per spec but reads terribly:
open()catches, deletes the orphaned session, and shows an error toast. The composer, however, appears to stay in bug mode wired to the now-deleted session — subsequent sends go nowhere. (App-side recovery gap; the composer should exit bug mode when the open fails, or never enter it untilopen-bug-reportlands.)Evidence: server
GET /commandlisted 41 commands, none namedreport-a-bug; the chat DB contained zero sessions withbug_reportmetadata despite multiple button clicks; the fix was staging the skill server-side.Suggested fixes
/command(or the boot-time equivalent) and, whenreport-a-bugis absent server-side, fail with an actionable error — "the opencode server's staged skills are stale (missing report-a-bug); restart/re-stage the server" — instead of a generic arm failure.skills/into its staging dir at boot — additive, no deletes — which closes the drift class for that deployment.)Refs: #251 (bug-session orchestration), ADR-0004, #295 (related server-misconfiguration guard, draft).
Implementation Plan
Layer 1: Pre-flight the arm (extension-side)
File:
packages/extension/src/bug_report.tsIn
open(), beforecreateSession(), GET/commandfrom the server and verifyreport-a-bugis in the response. If absent, show an actionable error and return without creating a session.New private method:
Call site (in
open(), beforecreateSession):Fail-open: if the GET itself fails (network, server busy), return
trueand let the existing arm-failure path handle it. Never block the happy path on a probe failure.Layer 2: Reactive liveness effect (app-side)
File:
packages/app/src/pages/session/composer/session-bug-dock.tsxAdd a
createEffectthat watches the session map when the dock is in"chat"phase. If the dock's currentsessionIDvanishes from the synced session-info (because the extension deleted it), auto-dismiss silently viacontroller.dismiss(). No new HTTP calls — rides the existing session-info subscription. No toast (the extension already showed one).Layer 3: ADR-0004 update
Append to "Accepted costs": the arm target (
/session/:id/command) resolves against the server's command set, not the extension's staged skill paths. A non-extension-hosted server must re-stage skills on extension updates; the extension pre-flights the arm and fails actionably when the skill is absent server-side. The fleet wrapper's additive rsync-at-boot closes this drift class.Acceptance Criteria
Scope
bug_report.ts(~15 lines new)session-bug-dock.tsx(~8 lines new)docs/adr/0004-bug-session-lifecycle.md(~3 lines appended)test/bug_report.test.ts(1 new describe block), app dock test (1 new case)