You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Actionable fixes to prevent the "no GUI response" failure class and harden the
webview-server connection lifecycle. Each item includes acceptance criteria.
See also:
./issue-no-gui-response.md — the specific bug these fixes address
../notes/local-storage-customization.md — parameter inventory and design analysis
../notes/fix-plan.md — terminal-mode fixes (subset included here as Tier 3)
Tier 1 — Critical Path
These fixes directly prevent or mitigate the "no GUI response" bug. They should be
prioritized for immediate implementation.
1. [CRITICAL] Remove defaultServerUrl localStorage override in webview context
File:packages/app/src/entry.tsx (lines 157–161)
Problem:getDefaultUrl() reads opencode.settings.dat:defaultServerUrl from
localStorage before consulting location.origin. In the Amicode webview, the
iframe is served by the running server — location.origin is always the correct
URL. But if a previous session wrote defaultServerUrl (pointing to a now-dead
port), it permanently overrides the correct origin.
Fix: When running inside the Amicode webview (detectable via inAmicode() from utils/amicode-bridge.ts), skip the localStorage defaultServerUrl lookup
entirely. Use location.origin unconditionally.
Acceptance criteria:
When inAmicode() returns true, getDefaultUrl() returns getCurrentUrl()
without consulting localStorage.
The defaultServerUrl localStorage key is still honored in the standalone web
app context (when inAmicode() is false).
After a server restart (new port), reopening the Amicode webview connects to the
new server without requiring localStorage to be cleared.
2. [CRITICAL] SSE reconnect escalation: fall back to location.origin after persistent failures
Problem: The SSE reconnect loop retries the same URL every 250 ms indefinitely.
It does not distinguish "connection refused" (server gone) from "server error"
(server exists). If the URL is stale, this loops forever without recovery.
Fix: Add an escalation path: after N consecutive connection-refused failures
(suggested: 10, i.e., 2.5 seconds), attempt to reconnect using location.origin
instead of the persisted server URL. If location.origin succeeds, update the
persisted server store to reflect the correct URL.
Acceptance criteria:
After 10 consecutive connection-refused errors (not HTTP errors — specifically TypeError: Failed to fetch or equivalent network failure), the loop switches to location.origin as the target URL.
If location.origin connects successfully, the persisted server store entry is
updated in-place so subsequent reconnections use the correct URL directly.
If both the persisted URL and location.origin fail, the loop continues retrying location.origin at 250 ms intervals (as today, but to the correct URL).
A streamStatus value of "reconnecting" or "discovering" is surfaced (for
the ConnectionBanner to display).
3. [CRITICAL] Server boot-ID stamping
Files:
Server: packages/opencode/src/server/server.ts or the SSE event handler
Client: packages/app/src/context/server-sdk.tsx and server.tsx
Problem: The client cannot distinguish "same server reconnected" from "different
server on same port" from "stale URL, server gone." Without a server identity
token, all reconnection heuristics are fragile.
Fix: The server generates a random boot-ID (e.g., UUID v4) at startup and
includes it in:
The server.connected SSE event payload (so the client receives it on every
reconnection).
A response header on all API responses (e.g., X-OpenCode-Boot-ID), so the
client can detect mid-session server restarts even outside the SSE stream.
The client persists the boot-ID alongside the server URL in localStorage. On
reconnection:
If boot-ID matches → normal reconnect; session state is valid.
If boot-ID differs → server restarted; invalidate session tab list, refetch all
state, and optionally re-validate credentials.
If boot-ID is absent (older server version) → treat as "unknown," fall back to
current behavior.
Acceptance criteria:
Server emits a bootId field in the server.connected event.
Server includes X-OpenCode-Boot-ID: <uuid> header on all HTTP responses.
Client stores lastBootId per server entry in the persisted store.
On boot-ID mismatch, client runs a "full refresh" path: refetch sessions list,
prune tabs with 404 sessions, reset workspace caches.
On boot-ID mismatch, if auth credentials produce 401, the client surfaces an
auth-required prompt (rather than silently failing).
4. [CRITICAL] Configurable fixed port per container
Problem: In a devcontainer, the server port is ephemeral (random or 4096+N).
Because the webview's localStorage persists across container rebuilds (it lives on
the host), a new port on restart means a stale persisted URL. Users in devcontainer
workflows hit this on every rebuild.
Fix: Allow users to pin a stable port per container via either:
opencode.json → { "server": { "port": 4096 } } (already supported by the
schema but not widely documented or surfaced).
.devcontainer.json → "containerEnv": { "OPENCODE_PORT": "4096" } (read by
the Amicode extension host when spawning the server).
Document the recommendation: in devcontainer-based workflows, set a fixed port so
that localStorage's persisted URL remains valid across container rebuilds.
Acceptance criteria:
Documentation (README or extension settings description) explicitly recommends
setting server.port in opencode.json for devcontainer workflows.
The Amicode extension host reads OPENCODE_PORT from the container environment
(if available) and uses it when launching the server.
When server.port is set in opencode.json, the server binds to exactly that
port (no fallback) and fails loudly if the port is in use (rather than silently
falling back to a random port).
The .devcontainer/devcontainer.json in this repo is updated to include a
commented-out example: "containerEnv": { "OPENCODE_PORT": "4096" }.
Tier 2 — High Importance
These fixes prevent related failure modes and harden the connection lifecycle.
Implement after Tier 1.
5. Multi-instance localStorage isolation
File:packages/app/src/utils/persist.ts
Problem: All Amicode webview instances on the same VS Code installation share a
single localStorage scope (keyed by extension ID origin). Two windows with
different servers overwrite each other's server entries.
Fix: Key all connection-related localStorage entries by a workspace
identifier (e.g., a hash of the container's filesystem root or the server URL at
first successful connection). Non-connection state (theme, zoom) remains global.
Acceptance criteria:
Two VS Code windows with Amicode, connected to different servers, do not
interfere with each other's connection state.
Opening a new window for a previously-unknown workspace starts fresh (no stale
entries from another workspace).
Global preferences (theme, solver mode) remain shared across all instances.
Migration: on first load with the new keying scheme, existing global state is
migrated into the appropriate workspace bucket.
6. Session tab validation on load
File:packages/app/src/context/tabs.tsx
Problem: Session tabs persist indefinitely (pruning only fires at 50+ keys).
Dead session references from previous server instances accumulate, causing burst
fetches to stale/non-existent endpoints on reload.
Fix: On the server.connected event (which fires on every SSE reconnection),
validate all open session tabs by checking their existence against the server. Tabs
whose session IDs return 404 are moved to the closed list (not deleted — user can
re-open if the session reappears after a migration/restore).
Acceptance criteria:
Within 5 seconds of server.connected, all open tabs are validated.
Tabs with 404 sessions are moved to closed with a reason: "not_found" marker.
A toast notification summarizes: "N sessions from a previous server instance were
closed."
The validation is non-blocking (does not prevent the app from rendering).
If the server is unreachable during validation (e.g., the server.connected
event was a false positive), validation is skipped gracefully.
Problem: If the server regenerates OPENCODE_SERVER_PASSWORD on restart,
persisted credentials in the localStorage server.list entry are stale. Every API
call returns 401, but the client does not surface this or attempt to refresh.
Fix: When the boot-ID changes (see item #3), clear persisted credentials for
that server entry and re-read them from the iframe URL query param (auth_token).
If no auth_token is present in the URL and the server requires auth, surface an
auth prompt.
Acceptance criteria:
On boot-ID mismatch, the persisted username/password for the affected server
entry are cleared.
The app re-reads auth_token from location.search (the iframe URL injected by
the extension host).
If auth is required and no valid credentials are available, a modal prompts the
user (rather than silently failing with 401s).
8. Extension host → webview URL push on server restart
File: The Amicode extension host (in harmoniqs/amicode repo — not this repo)
Problem: When the extension host restarts the server (via amicode.restartServer command), it re-launches the opencode process on a
potentially different port. The webview SSE stream is connected to the old port and
must wait for connection-refused → escalation (item #2) to recover.
Fix: The extension host should postMessage a { source: "amicode", kind: "server-url-changed", url: "http://localhost:NEW_PORT" } message to the webview
iframe immediately after the new server is confirmed listening. The webview handles
this message by updating its active server URL and immediately reconnecting SSE to
the new URL.
Acceptance criteria:
The webview registers a listener for kind: "server-url-changed" messages.
On receiving this message, the webview updates its persisted server store and
reconnects SSE within 1 second (no 250 ms retry loop needed).
If the message arrives while the webview is already connected (race condition),
it is a no-op.
The webview emits a route-info message back to confirm it received the update.
Tier 3 — Improvements
These are well-advised hardening measures. They do not directly prevent the "no GUI
response" bug but reduce adjacent failure surfaces.
Problem: The localStorage eviction logic removes the largest opencode.* keys
first. The server key (connection state) and tabs key (session history) grow
over time and become prime eviction targets.
Fix: Maintain a "protected keys" list that the eviction logic never removes.
At minimum: opencode.global.dat:server, opencode.settings.dat:defaultServerUrl.
Acceptance criteria:
opencode.global.dat:server is never evicted by the quota handler.
Eviction preferentially targets workspace and session-scoped keys.
If eviction cannot free enough space without touching protected keys, the write
fails gracefully (the app continues to function with the existing state).
10. Connection banner always visible on persistent disconnection
Problem: The ConnectionBanner component shows when streamStatus is "disconnected", but its visibility depends on layout configuration. If the banner
is scrolled off or hidden by a panel, the user has no indication that the
connection is broken.
Fix: After 5 seconds of continuous "disconnected" state, surface a
VS Code-style notification (via postMessage to the extension host, which calls vscode.window.showWarningMessage) in addition to the in-webview banner.
Acceptance criteria:
If the SSE stream is disconnected for > 5 continuous seconds, a warning
notification appears in VS Code's notification area.
Problem: The retry loop tries 10 times (2 s total). If the server doesn't
respond (port collision, slow startup), the file reference is silently dropped.
Fix: Increase to 20 retries (4 s). After the loop, if not connected, show vscode.window.showWarningMessage(...) so the user knows something went wrong.
Acceptance criteria:
The retry window is 4 seconds (20 * 200 ms).
If connected is still false after the loop, a warning message is shown.
The warning message suggests "try again" or "the port may be in use."
Actionable fixes to prevent the "no GUI response" failure class and harden the
webview-server connection lifecycle. Each item includes acceptance criteria.
See also:
./issue-no-gui-response.md— the specific bug these fixes address../notes/local-storage-customization.md— parameter inventory and design analysis../notes/fix-plan.md— terminal-mode fixes (subset included here as Tier 3)Tier 1 — Critical Path
These fixes directly prevent or mitigate the "no GUI response" bug. They should be
prioritized for immediate implementation.
1. [CRITICAL] Remove
defaultServerUrllocalStorage override in webview contextFile:
packages/app/src/entry.tsx(lines 157–161)Problem:
getDefaultUrl()readsopencode.settings.dat:defaultServerUrlfromlocalStorage before consulting
location.origin. In the Amicode webview, theiframe is served by the running server —
location.originis always the correctURL. But if a previous session wrote
defaultServerUrl(pointing to a now-deadport), it permanently overrides the correct origin.
Fix: When running inside the Amicode webview (detectable via
inAmicode()fromutils/amicode-bridge.ts), skip the localStoragedefaultServerUrllookupentirely. Use
location.originunconditionally.Acceptance criteria:
inAmicode()returnstrue,getDefaultUrl()returnsgetCurrentUrl()without consulting localStorage.
defaultServerUrllocalStorage key is still honored in the standalone webapp context (when
inAmicode()isfalse).new server without requiring localStorage to be cleared.
2. [CRITICAL] SSE reconnect escalation: fall back to
location.originafter persistent failuresFile:
packages/app/src/context/server-sdk.tsx(lines 268–338)Problem: The SSE reconnect loop retries the same URL every 250 ms indefinitely.
It does not distinguish "connection refused" (server gone) from "server error"
(server exists). If the URL is stale, this loops forever without recovery.
Fix: Add an escalation path: after N consecutive connection-refused failures
(suggested: 10, i.e., 2.5 seconds), attempt to reconnect using
location.origininstead of the persisted server URL. If
location.originsucceeds, update thepersisted server store to reflect the correct URL.
Acceptance criteria:
TypeError: Failed to fetchor equivalent network failure), the loop switches tolocation.originas the target URL.location.originconnects successfully, the persistedserverstore entry isupdated in-place so subsequent reconnections use the correct URL directly.
location.originfail, the loop continues retryinglocation.originat 250 ms intervals (as today, but to the correct URL).streamStatusvalue of"reconnecting"or"discovering"is surfaced (forthe ConnectionBanner to display).
3. [CRITICAL] Server boot-ID stamping
Files:
packages/opencode/src/server/server.tsor the SSE event handlerpackages/app/src/context/server-sdk.tsxandserver.tsxProblem: The client cannot distinguish "same server reconnected" from "different
server on same port" from "stale URL, server gone." Without a server identity
token, all reconnection heuristics are fragile.
Fix: The server generates a random boot-ID (e.g., UUID v4) at startup and
includes it in:
server.connectedSSE event payload (so the client receives it on everyreconnection).
X-OpenCode-Boot-ID), so theclient can detect mid-session server restarts even outside the SSE stream.
The client persists the boot-ID alongside the server URL in localStorage. On
reconnection:
state, and optionally re-validate credentials.
current behavior.
Acceptance criteria:
bootIdfield in theserver.connectedevent.X-OpenCode-Boot-ID: <uuid>header on all HTTP responses.lastBootIdper server entry in the persisted store.prune tabs with 404 sessions, reset workspace caches.
auth-required prompt (rather than silently failing).
4. [CRITICAL] Configurable fixed port per container
Files:
packages/core/src/v1/config/server.ts.devcontainer/devcontainer.jsonProblem: In a devcontainer, the server port is ephemeral (random or 4096+N).
Because the webview's localStorage persists across container rebuilds (it lives on
the host), a new port on restart means a stale persisted URL. Users in devcontainer
workflows hit this on every rebuild.
Fix: Allow users to pin a stable port per container via either:
opencode.json→{ "server": { "port": 4096 } }(already supported by theschema but not widely documented or surfaced).
.devcontainer.json→"containerEnv": { "OPENCODE_PORT": "4096" }(read bythe Amicode extension host when spawning the server).
Document the recommendation: in devcontainer-based workflows, set a fixed port so
that localStorage's persisted URL remains valid across container rebuilds.
Acceptance criteria:
setting
server.portinopencode.jsonfor devcontainer workflows.OPENCODE_PORTfrom the container environment(if available) and uses it when launching the server.
server.portis set inopencode.json, the server binds to exactly thatport (no fallback) and fails loudly if the port is in use (rather than silently
falling back to a random port).
.devcontainer/devcontainer.jsonin this repo is updated to include acommented-out example:
"containerEnv": { "OPENCODE_PORT": "4096" }.Tier 2 — High Importance
These fixes prevent related failure modes and harden the connection lifecycle.
Implement after Tier 1.
5. Multi-instance localStorage isolation
File:
packages/app/src/utils/persist.tsProblem: All Amicode webview instances on the same VS Code installation share a
single localStorage scope (keyed by extension ID origin). Two windows with
different servers overwrite each other's
serverentries.Fix: Key all connection-related localStorage entries by a workspace
identifier (e.g., a hash of the container's filesystem root or the server URL at
first successful connection). Non-connection state (theme, zoom) remains global.
Acceptance criteria:
interfere with each other's connection state.
entries from another workspace).
migrated into the appropriate workspace bucket.
6. Session tab validation on load
File:
packages/app/src/context/tabs.tsxProblem: Session tabs persist indefinitely (pruning only fires at 50+ keys).
Dead session references from previous server instances accumulate, causing burst
fetches to stale/non-existent endpoints on reload.
Fix: On the
server.connectedevent (which fires on every SSE reconnection),validate all open session tabs by checking their existence against the server. Tabs
whose session IDs return 404 are moved to the
closedlist (not deleted — user canre-open if the session reappears after a migration/restore).
Acceptance criteria:
server.connected, all open tabs are validated.closedwith areason: "not_found"marker.closed."
server.connectedevent was a false positive), validation is skipped gracefully.
7. Credential invalidation on boot-ID change
File:
packages/app/src/context/server.tsx(insideresolveServerList)Problem: If the server regenerates
OPENCODE_SERVER_PASSWORDon restart,persisted credentials in the localStorage
server.listentry are stale. Every APIcall returns 401, but the client does not surface this or attempt to refresh.
Fix: When the boot-ID changes (see item #3), clear persisted credentials for
that server entry and re-read them from the iframe URL query param (
auth_token).If no
auth_tokenis present in the URL and the server requires auth, surface anauth prompt.
Acceptance criteria:
username/passwordfor the affected serverentry are cleared.
auth_tokenfromlocation.search(the iframe URL injected bythe extension host).
user (rather than silently failing with 401s).
8. Extension host → webview URL push on server restart
File: The Amicode extension host (in
harmoniqs/amicoderepo — not this repo)Problem: When the extension host restarts the server (via
amicode.restartServercommand), it re-launches the opencode process on apotentially different port. The webview SSE stream is connected to the old port and
must wait for connection-refused → escalation (item #2) to recover.
Fix: The extension host should
postMessagea{ source: "amicode", kind: "server-url-changed", url: "http://localhost:NEW_PORT" }message to the webviewiframe immediately after the new server is confirmed listening. The webview handles
this message by updating its active server URL and immediately reconnecting SSE to
the new URL.
Acceptance criteria:
kind: "server-url-changed"messages.reconnects SSE within 1 second (no 250 ms retry loop needed).
it is a no-op.
route-infomessage back to confirm it received the update.Tier 3 — Improvements
These are well-advised hardening measures. They do not directly prevent the "no GUI
response" bug but reduce adjacent failure surfaces.
9. Quota-aware eviction priority
File:
packages/app/src/utils/persist.ts(lines 112–165)Problem: The localStorage eviction logic removes the largest
opencode.*keysfirst. The
serverkey (connection state) andtabskey (session history) growover time and become prime eviction targets.
Fix: Maintain a "protected keys" list that the eviction logic never removes.
At minimum:
opencode.global.dat:server,opencode.settings.dat:defaultServerUrl.Acceptance criteria:
opencode.global.dat:serveris never evicted by the quota handler.workspaceandsession-scoped keys.fails gracefully (the app continues to function with the existing state).
10. Connection banner always visible on persistent disconnection
File:
packages/app/src/components/connection-banner.tsxProblem: The
ConnectionBannercomponent shows whenstreamStatusis"disconnected", but its visibility depends on layout configuration. If the banneris scrolled off or hidden by a panel, the user has no indication that the
connection is broken.
Fix: After 5 seconds of continuous
"disconnected"state, surface aVS Code-style notification (via
postMessageto the extension host, which callsvscode.window.showWarningMessage) in addition to the in-webview banner.Acceptance criteria:
notification appears in VS Code's notification area.
rediscovery from item β.2 — Vendor + pin opencode in the VSIX #2).
11. Terminal extension: readiness probe fix (
/app→/health)File:
sdks/vscode/src/extension.ts(line 78)Problem: The extension probes
GET /app(a catch-all UI route) with nostatus-code check. Should probe
GET /healthand checkresponse.ok.Fix: Change the URL to
/healthand gateconnected = trueonresponse.ok.Acceptance criteria:
GET /health.connectedis only set totrueif the response status is 2xx.connected = true.12. Terminal extension: dead terminal detection
File:
sdks/vscode/src/extension.ts(lines 15–19)Problem:
opencode.openTerminalreuses a terminal by name without checking ifthe process has exited.
Fix: Filter
vscode.window.terminalsby bothname === TERMINAL_NAMEandexitStatus === undefined.Acceptance criteria:
13. RPC error propagation
File:
packages/opencode/src/util/rpc.ts(lines 5–12, 23, 47)Problem: If a worker-side RPC method throws, the pending promise in
client.call()is never settled. The caller hangs forever.Fix:
rpc[method](input)in try/catch; postrpc.errormessagewith the error details and request ID.
{ resolve, reject }pairs inpending; handlerpc.errormessages by calling
reject(new Error(...)).Acceptance criteria:
containing the original error message.
pendingMap entry is cleaned up (no memory leak).client.call()that do not handle rejection see an unhandledrejection (which is logged by item feat: plot the pulse with Piccolo plot_pulse (not hand-rolled Makie) #14) rather than hanging forever.
14. Worker error logging
File:
packages/opencode/src/cli/tui/worker.ts(lines 16–21)Problem:
unhandledRejectionanduncaughtExceptionhandlers discard allerrors silently, making worker failures invisible.
Fix: Log errors to
stderrwith a[worker]prefix.Acceptance criteria:
is preserved).
15. Terminal extension: retry window + user-visible warning
File:
sdks/vscode/src/extension.ts(lines 73–90)Problem: The retry loop tries 10 times (2 s total). If the server doesn't
respond (port collision, slow startup), the file reference is silently dropped.
Fix: Increase to 20 retries (4 s). After the loop, if not connected, show
vscode.window.showWarningMessage(...)so the user knows something went wrong.Acceptance criteria:
connectedis stillfalseafter the loop, a warning message is shown.