From e3797346b194e3dc0886e8af5e534e6e9e57b396 Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Sat, 12 Sep 2026 10:05:50 -0400 Subject: [PATCH] feat(devtools): structured rebuild error panel (amicode #1022) Replace the single-span truncated-stderr error display in Developer Tools with a structured error panel: Controller (developer-tools-controller.ts): - New RebuildErrorInfo type: { message, fix[], detail? } - rebuildError signal changed from string to RebuildErrorInfo - Accepts both structured (new) and flat-string (legacy) errors from the bridge for backward compatibility - Timeout error now carries fix steps Component (developer-tools.tsx): - RebuildStatusIndicator split into RebuildStatusBadge (inline dot + label in heading, shows only 'Failed') and RebuildErrorPanel (block below rebuild buttons) - RebuildErrorPanel renders: error message (red, bold), numbered fix steps (ordered list), collapsible detail (Show/Hide details toggle with monospace pre block) CSS (amicode.css): - .devtools-error-panel: tinted danger background with border - .devtools-error-message: flexbox with status dot - .devtools-error-fix-steps: numbered list with secondary text - .devtools-error-detail-toggle: minimal button - .devtools-error-detail: monospace scrollable pre block All styles use existing theme variables (--text-danger-base, --surface-danger-strong, --text-secondary-base) for light/dark compat. --- .../settings-v2/developer-tools-controller.ts | 36 ++++++++++- .../settings-v2/developer-tools.tsx | 44 ++++++++++++-- packages/ui/src/amicode/amicode.css | 60 +++++++++++++++++++ 3 files changed, 132 insertions(+), 8 deletions(-) diff --git a/packages/app/src/components/settings-v2/developer-tools-controller.ts b/packages/app/src/components/settings-v2/developer-tools-controller.ts index 179cb25ae..1f605ca81 100644 --- a/packages/app/src/components/settings-v2/developer-tools-controller.ts +++ b/packages/app/src/components/settings-v2/developer-tools-controller.ts @@ -15,6 +15,16 @@ export interface DevToolsStatus { export type RebuildState = "idle" | "rebuilding" | "rebuilt" | "failed" +/** Structured rebuild error from the catalog (rebuild_errors.ts). */ +export interface RebuildErrorInfo { + /** One-line error message. */ + message: string + /** Numbered fix steps. */ + fix: string[] + /** Raw stderr / stack trace (collapsible). */ + detail?: string +} + /** Default repo paths autofilled when the toggle is turned ON with empty fields. */ const DEFAULT_OPENCODE_PATH = "~/harmoniqs/opencode" const DEFAULT_AMICODE_PATH = "~/harmoniqs/amicode" @@ -24,7 +34,7 @@ export function createDeveloperToolsController() { const [status, setStatus] = createSignal(undefined) const [pending, setPending] = createSignal(false) const [rebuildState, setRebuildState] = createSignal("idle") - const [rebuildError, setRebuildError] = createSignal(undefined) + const [rebuildError, setRebuildError] = createSignal(undefined) const [vsixBuildState, setVsixBuildState] = createSignal("idle") const [vsixBuildError, setVsixBuildError] = createSignal(undefined) const [vsixPath, setVsixPath] = createSignal(undefined) @@ -50,7 +60,14 @@ export function createDeveloperToolsController() { if (rebuildState() === "rebuilding") { applyRebuildFlagMutation(rebuildFlagMutation("failed")) setRebuildState("failed") - setRebuildError("Rebuild timed out") + setRebuildError({ + message: "Rebuild timed out", + fix: [ + "Close the Settings dialog and check the 'Amicode — opencode' output channel.", + "Try the rebuild again.", + "If it keeps timing out, report the issue.", + ], + }) } }, 300_000) } else if (didFinish) { @@ -101,7 +118,20 @@ export function createDeveloperToolsController() { } else if (d.state === "failed") { applyRebuildFlagMutation(rebuildFlagMutation("failed")) setRebuildState("failed") - setRebuildError(d.error ?? "Unknown error") + // Accept structured errors (new) or flat strings (legacy bridge compat). + if (d.error && typeof d.error === "object" && typeof d.error.message === "string") { + setRebuildError({ + message: d.error.message, + fix: Array.isArray(d.error.fix) ? d.error.fix : [], + detail: typeof d.error.detail === "string" ? d.error.detail : undefined, + }) + } else { + setRebuildError({ + message: typeof d.error === "string" ? d.error : "Unknown error", + fix: [], + detail: undefined, + }) + } } else if (d.state === "done") { // The extension host confirmed the build finished — set the // "rebuilt" flag now (not at rebuild-start) so a dialog reopened diff --git a/packages/app/src/components/settings-v2/developer-tools.tsx b/packages/app/src/components/settings-v2/developer-tools.tsx index df62b0216..e9c67dcbf 100644 --- a/packages/app/src/components/settings-v2/developer-tools.tsx +++ b/packages/app/src/components/settings-v2/developer-tools.tsx @@ -1,4 +1,4 @@ -import { Component, Match, Show, Switch } from "solid-js" +import { Component, For, Match, Show, Switch, createSignal } from "solid-js" import { Switch as ToggleSwitch } from "@opencode-ai/ui/v2/switch-v2" import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2" import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2" @@ -11,8 +11,8 @@ import { type DeveloperToolsController, } from "./developer-tools-controller" -/** Status indicator shown below the section title during/after rebuilds. */ -const RebuildStatusIndicator: Component<{ controller: DeveloperToolsController }> = (props) => { +/** Inline status dot shown next to the heading during/after rebuilds. */ +const RebuildStatusBadge: Component<{ controller: DeveloperToolsController }> = (props) => { const language = useLanguage() return ( @@ -31,13 +31,46 @@ const RebuildStatusIndicator: Component<{ controller: DeveloperToolsController }
- {props.controller.rebuildError() ?? "Build failed"} + Failed
) } +/** Block error panel shown below the rebuild buttons when a rebuild fails. */ +const RebuildErrorPanel: Component<{ controller: DeveloperToolsController }> = (props) => { + const [detailOpen, setDetailOpen] = createSignal(false) + const err = () => props.controller.rebuildError() + + return ( + +
+
+ + {err()!.message} +
+ 0}> +
    + {(step) =>
  1. {step}
  2. }
    +
+
+ + + +
{err()!.detail}
+
+
+
+
+ ) +} + const DeveloperToolsContent: Component<{ controller: DeveloperToolsController }> = (props) => { const language = useLanguage() const opencodeError = () => { @@ -76,6 +109,7 @@ const DeveloperToolsContent: Component<{ controller: DeveloperToolsController }> {language.t("settings.general.row.devTools.rebuildRemotely")} +
@@ -235,7 +269,7 @@ export const DeveloperToolsSection: Component = () => {

{language.t("settings.general.section.developerTools")}

- +
diff --git a/packages/ui/src/amicode/amicode.css b/packages/ui/src/amicode/amicode.css index 034dd36f2..e002332b0 100644 --- a/packages/ui/src/amicode/amicode.css +++ b/packages/ui/src/amicode/amicode.css @@ -1520,6 +1520,66 @@ padding: 16px 0 4px; } +/* ---- Structured rebuild error panel (#1022) ──────────────────────────── */ +.devtools-error-panel { + margin: 8px 0 4px; + padding: 10px 12px; + border-radius: 6px; + background: color-mix(in srgb, var(--surface-danger-strong, #e5484d) 8%, transparent); + border: 1px solid color-mix(in srgb, var(--surface-danger-strong, #e5484d) 20%, transparent); +} +.devtools-error-message { + display: flex; + align-items: flex-start; + gap: 6px; + font-size: 13px; + font-weight: 600; + color: var(--text-danger-base, #e5484d); + line-height: 1.4; +} +.devtools-error-message .devtools-status-dot { + margin-top: 4px; + flex-shrink: 0; +} +.devtools-error-fix-steps { + margin: 6px 0 0; + padding-left: 22px; + font-size: 12px; + line-height: 1.6; + color: var(--text-secondary-base, #8b8b8b); + list-style: decimal; +} +.devtools-error-fix-steps li { + padding-left: 4px; +} +.devtools-error-detail-toggle { + margin-top: 6px; + padding: 0; + border: none; + background: none; + font-size: 11px; + color: var(--text-secondary-base, #8b8b8b); + cursor: pointer; + font-family: inherit; +} +.devtools-error-detail-toggle:hover { + color: var(--text-primary-base, #ededed); +} +.devtools-error-detail { + margin-top: 4px; + padding: 8px; + border-radius: 4px; + background: var(--surface-secondary-base, rgba(0,0,0,0.15)); + font-size: 11px; + line-height: 1.5; + color: var(--text-secondary-base, #8b8b8b); + font-family: var(--font-mono, monospace); + overflow-x: auto; + max-height: 200px; + white-space: pre-wrap; + word-break: break-all; +} + /* ───────────────────────────────────────────────────────────────────────────── APPROVAL CARD — [data-component="amicode-approval-card"] (approval-card.tsx) The capability-warrant card (spec-20260727-164748 §9.5). Sibling of the ask