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
15 changes: 15 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,21 @@ jobs:
cd gui
bun run build

- name: Record dashboard preview source
if: needs.changes.outputs.gui == 'true'
run: |
git rev-parse HEAD > gui/dist/build-commit.txt
git rev-parse HEAD:gui > gui/dist/build-gui-tree.txt

- name: Upload dashboard preview
if: needs.changes.outputs.gui == 'true'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: dashboard-preview-${{ github.sha }}
path: gui/dist
retention-days: 7
if-no-files-found: error

- name: CLI help smoke
run: bun run src/cli/index.ts help

Expand Down
10 changes: 9 additions & 1 deletion devlog/_plan/260906_release_244_followups/090_dashboard.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Dashboard alignment carry

Depends on integrated runtime for final presentation; independent PR, class C2. Carry #3697 head 4c7d6a5e07a29f7ef833019304c11343b94d102f, preserving Co-authored-by: Robin Bially <7304732+RobinBially@users.noreply.github.com>. #3689 authless-default change is outside this train.
Depends on integrated runtime for final presentation; independent PR, class C2. Carry #3697 head 49a9c79392babd9413831437d6ad71839737b148 (base cededd5ad1b8f8c437813c315c0705ace6c950c3), preserving Co-authored-by: Robin Bially <7304732+RobinBially@users.noreply.github.com>. #3689 authless-default change is outside this train.

## Exact change map

Expand All @@ -21,3 +21,11 @@ Remote GUI lint/stylelint, GUI tests and Vite build from ci.yml; verify rendered

No authless setting, quota semantics or model management expansion. Preserve current state labels and accessibility. P rechecks any intervening same-file changes before carrying.


## Hosted artifact verification

Main owns the eight-file attributed carry; no handler or visible-copy changes. Add one bounded preview artifact to the existing ci.yml gates job after its existing GUI build, only when changes.outputs.gui is true. Pin actions/upload-artifact v7.0.1 to043fb46d1a93c77aae656e7c1c64a875d1fc6a0a (verified official repository tag/release). Include only gui/dist, with generated build-commit.txt and build-gui-tree.txt; retention7days and missing-fileserror. No trigger, token permission, secret, checkout or release change. Workflow surface raises this unit to C4 and gets independent security review.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

The artifact identifies the actual checkout and gui tree: PR CI may build a merge ref. Compare build-gui-tree against final reviewed head:gui before claiming final UI proof. Download that artifact, serve the built frontend with an isolated fixture API in scratch (no real provider, account, or service mutation), and use the existing browser runtime for1440/1024/768/390/320px and keyboard/focus/overflow observation. No local build/typecheck/test suite. Public screenshots use synthetic data only; the source PR jpg is a reference, not final-head evidence.

Maintain manual dependent PR publication after the owner's explicit native-stack removal. AsyncCI/admin integration of3762/3763 is delegated; no local rebase. Await artifact only for visual C, not for unrelated bookkeeping. HostedCI handles lint/typecheck/tests/build/privacy. Escalate only an actual permission or unavailable artifact boundary; no invented local fallback. No user token/cost cap; each tooling run bounded30minutes, individual waits <=60seconds; main retains complete release goal.
7 changes: 7 additions & 0 deletions docs-site/src/content/docs/guides/web-dashboard.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,13 @@ missing credential — the proxy did not recognise the request as loopback. Open
the address the proxy prints on startup (usually `http://127.0.0.1:<port>`), and prefer that exact
host and port over a LAN IP or an alias.

## Dashboard layout

Overview uses matching status cards and full-width settings rows. On wide screens, labels share
one column and model/effort controls share another. On narrower screens, controls move below their
labels in the same reading order. Long version labels are shortened visually; hover the version
badge or the Version card to read the full value.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

## What you can do

| Area | What it does |
Expand Down
Binary file added docs/pr-assets/dashboard-settings-aligned.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion gui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ export default function App() {
>
<span className="brand-logo" role="img" aria-label={t("app.logoAria")} />
<span className="name">opencodex</span>
<span className="ver">v{displayedVersion}</span>
<span className="ver" title={displayedVersion}>v{displayedVersion}</span>
</button>
);

Expand Down
10 changes: 5 additions & 5 deletions gui/src/pages/dashboard-overview-head.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 +48,16 @@ export function DashboardOverviewHead({
<IconInfo width={14} height={14} aria-hidden="true" />
</button>
</div>
<div className="value" style={{ display: "flex", alignItems: "center", justifyContent: "center" }}>
<div role="radiogroup" aria-label={t("dash.multiAgent")} style={{ display: "inline-flex", borderRadius: "var(--radius-pill)", background: "var(--surface-soft, var(--raised))", padding: 3, gap: 2 }}>
<div className="value" style={{ display: "flex", alignItems: "center" }}>
<div className="dash-ma-switch" role="radiogroup" aria-label={t("dash.multiAgent")} style={{ display: "inline-flex", borderRadius: "var(--radius-pill)", background: "var(--surface-soft, var(--raised))", padding: 3, gap: 2 }}>
{(["v1", "default", "v2"] as const).map(mode => (
<button
key={mode}
type="button"
role="radio"
aria-checked={maMode === mode}
className={`btn btn-sm text-caption${maMode === mode ? " btn-primary" : " btn-ghost"}`}
style={{ borderRadius: "var(--radius-pill)", minWidth: 36, padding: "5px 10px", border: "none", background: maMode === mode ? undefined : "transparent", color: maMode === mode ? undefined : "var(--muted)" }}
className={`btn btn-sm text-caption dash-ma-option${maMode === mode ? " btn-primary" : " btn-ghost"}`}
style={{ borderRadius: "var(--radius-pill)", border: "none", background: maMode === mode ? undefined : "transparent", color: maMode === mode ? undefined : "var(--muted)" }}
disabled={maBusy}
onClick={() => void switchMaMode(mode)}
>{t(`models.v2Mode_${mode}` as TKey)}</button>
Expand All @@ -76,7 +76,7 @@ export function DashboardOverviewHead({
<span className={`dot ${online ? "dot-green" : "dot-red"}`} />{online ? t("dash.online") : t("dash.offline")}
</div>
</div>
<div className="stat" aria-busy={healthLoading || undefined}><div className="label">{t("dash.version")}</div><div className="value mono">{health?.version ?? "—"}</div></div>
<div className="stat" aria-busy={healthLoading || undefined}><div className="label">{t("dash.version")}</div><div className="value mono dash-stat-version" title={health?.version}>{health?.version ?? "—"}</div></div>
<div className="stat" aria-busy={healthLoading || undefined}><div className="label">{t("dash.uptime")}</div><div className="value mono">{health ? formatUptime(health.uptime, locale) : "—"}</div></div>
<div className="stat" aria-busy={healthLoading || undefined}><div className="label">{t("dash.providers")}</div><div className="value">{providers.length}</div></div>
<div className="stat" aria-busy={usageLoading || undefined}>
Expand Down
108 changes: 55 additions & 53 deletions gui/src/pages/dashboard-overview-sections.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export function DashboardEffortCapPanel({ apiBase, d }: { apiBase: string; d: Da
if (!maModeResolved || maMode === "v1") return null;

return (
<div className="panel">
<div className="panel dash-effort-panel">
<div className="injection-head">
<span className="injection-label" style={{ display: "inline-flex", alignItems: "center", gap: 6 }}>
{t("dash.effortCapLabel")}
Expand All @@ -62,56 +62,58 @@ export function DashboardEffortCapPanel({ apiBase, d }: { apiBase: string; d: Da
<IconInfo width={13} height={13} aria-hidden="true" />
</button>
</span>
<Select
value={effortCap}
options={[
{ value: "", label: t("dash.effortCapNone") },
...EFFORT_CAP_LEVELS.map(e => ({ value: e, label: e })),
]}
onChange={async (v) => {
if (effortCapSaving) return;
setEffortCapSaving(true);
try {
const res = await fetch(`${apiBase}/api/effort-caps`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ effortCap: v || null }),
});
const data = await requireJson<{ ok: boolean; effortCap?: string | null; subagentEffortCap?: string | null }>(res);
setEffortCap(data.effortCap ?? "");
setSubagentEffortCap(data.subagentEffortCap ?? "");
} catch { /* ignore */ }
finally { setEffortCapSaving(false); }
}}
disabled={effortCapSaving}
label={t("dash.effortCapLabel")}
align="right"
/>
<Select
value={subagentEffortCap}
options={[
{ value: "", label: t("dash.effortCapNone") },
...EFFORT_CAP_LEVELS.map(e => ({ value: e, label: e })),
]}
onChange={async (v) => {
if (effortCapSaving) return;
setEffortCapSaving(true);
try {
const res = await fetch(`${apiBase}/api/effort-caps`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ subagentEffortCap: v || null }),
});
const data = await requireJson<{ ok: boolean; effortCap?: string | null; subagentEffortCap?: string | null }>(res);
setEffortCap(data.effortCap ?? "");
setSubagentEffortCap(data.subagentEffortCap ?? "");
} catch { /* ignore */ }
finally { setEffortCapSaving(false); }
}}
disabled={effortCapSaving}
label={t("dash.subagentEffortCapLabel")}
align="right"
/>
<div className="dash-effort-controls">
<Select
value={effortCap}
options={[
{ value: "", label: t("dash.effortCapNone") },
...EFFORT_CAP_LEVELS.map(e => ({ value: e, label: e })),
]}
onChange={async (v) => {
if (effortCapSaving) return;
setEffortCapSaving(true);
try {
const res = await fetch(`${apiBase}/api/effort-caps`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ effortCap: v || null }),
});
const data = await requireJson<{ ok: boolean; effortCap?: string | null; subagentEffortCap?: string | null }>(res);
setEffortCap(data.effortCap ?? "");
setSubagentEffortCap(data.subagentEffortCap ?? "");
} catch { /* ignore */ }
finally { setEffortCapSaving(false); }
}}
disabled={effortCapSaving}
label={t("dash.effortCapLabel")}
align="right"
/>
<Select
value={subagentEffortCap}
options={[
{ value: "", label: t("dash.effortCapNone") },
...EFFORT_CAP_LEVELS.map(e => ({ value: e, label: e })),
]}
onChange={async (v) => {
if (effortCapSaving) return;
setEffortCapSaving(true);
try {
const res = await fetch(`${apiBase}/api/effort-caps`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ subagentEffortCap: v || null }),
});
const data = await requireJson<{ ok: boolean; effortCap?: string | null; subagentEffortCap?: string | null }>(res);
setEffortCap(data.effortCap ?? "");
setSubagentEffortCap(data.subagentEffortCap ?? "");
} catch { /* ignore */ }
finally { setEffortCapSaving(false); }
}}
disabled={effortCapSaving}
label={t("dash.subagentEffortCapLabel")}
align="right"
/>
</div>
</div>
</div>
);
Expand Down Expand Up @@ -625,7 +627,7 @@ export function DashboardSidecarPanels({ d }: { d: Dash }) {

<div className="panel" aria-busy={!shadowCall || undefined}>
<div className="spread" style={{ alignItems: "center" }}>
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<div className="dash-shadow-label" style={{ display: "flex", alignItems: "center", gap: 8 }}>
<span className="font-semibold">{t("dash.shadowCallIntercept")}</span>
<button
ref={shadowCallHelpTriggerRef}
Expand All @@ -642,7 +644,7 @@ export function DashboardSidecarPanels({ d }: { d: Dash }) {
</button>
<code className="muted text-caption">{`⚠ ${shadowSourceModelBadge(shadowCall?.sourceModels)}`}</code>
</div>
<div className="setting-controls" style={{ display: "flex", gap: 8, alignItems: "center" }}>
<div className="setting-controls dash-shadow-controls">
<button
type="button"
className={`switch ${shadowCall?.enabled ? "on" : ""}`}
Expand Down
Loading
Loading