Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -24,7 +34,7 @@ export function createDeveloperToolsController() {
const [status, setStatus] = createSignal<DevToolsStatus | undefined>(undefined)
const [pending, setPending] = createSignal(false)
const [rebuildState, setRebuildState] = createSignal<RebuildState>("idle")
const [rebuildError, setRebuildError] = createSignal<string | undefined>(undefined)
const [rebuildError, setRebuildError] = createSignal<RebuildErrorInfo | undefined>(undefined)
const [vsixBuildState, setVsixBuildState] = createSignal<RebuildState>("idle")
const [vsixBuildError, setVsixBuildError] = createSignal<string | undefined>(undefined)
const [vsixPath, setVsixPath] = createSignal<string | undefined>(undefined)
Expand All @@ -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) {
Expand Down Expand Up @@ -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
Expand Down
44 changes: 39 additions & 5 deletions packages/app/src/components/settings-v2/developer-tools.tsx
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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 (
<Switch>
Expand All @@ -31,13 +31,46 @@ const RebuildStatusIndicator: Component<{ controller: DeveloperToolsController }
<Match when={props.controller.rebuildState() === "failed"}>
<div class="devtools-rebuild-status devtools-rebuild-status--failed">
<span class="devtools-status-dot devtools-status-dot--red" />
<span>{props.controller.rebuildError() ?? "Build failed"}</span>
<span>Failed</span>
</div>
</Match>
</Switch>
)
}

/** 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 (
<Show when={props.controller.rebuildState() === "failed" && err()}>
<div class="devtools-error-panel">
<div class="devtools-error-message">
<span class="devtools-status-dot devtools-status-dot--red" />
<span>{err()!.message}</span>
</div>
<Show when={err()!.fix.length > 0}>
<ol class="devtools-error-fix-steps">
<For each={err()!.fix}>{(step) => <li>{step}</li>}</For>
</ol>
</Show>
<Show when={err()!.detail}>
<button
class="devtools-error-detail-toggle"
onClick={() => setDetailOpen(!detailOpen())}
>
{detailOpen() ? "▾ Hide details" : "▸ Show details"}
</button>
<Show when={detailOpen()}>
<pre class="devtools-error-detail">{err()!.detail}</pre>
</Show>
</Show>
</div>
</Show>
)
}

const DeveloperToolsContent: Component<{ controller: DeveloperToolsController }> = (props) => {
const language = useLanguage()
const opencodeError = () => {
Expand Down Expand Up @@ -76,6 +109,7 @@ const DeveloperToolsContent: Component<{ controller: DeveloperToolsController }>
{language.t("settings.general.row.devTools.rebuildRemotely")}
</ButtonV2>
</div>
<RebuildErrorPanel controller={props.controller} />
</Show>
<Show when={props.controller.devcontainerMode()}>
<div class="devtools-rebuild-row">
Expand Down Expand Up @@ -235,7 +269,7 @@ export const DeveloperToolsSection: Component = () => {
<h3 class="settings-v2-section-title">
{language.t("settings.general.section.developerTools")}
</h3>
<RebuildStatusIndicator controller={controller} />
<RebuildStatusBadge controller={controller} />
</div>
<DeveloperToolsContent controller={controller} />

Expand Down
60 changes: 60 additions & 0 deletions packages/ui/src/amicode/amicode.css
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading