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
68 changes: 68 additions & 0 deletions devlog/_plan/261005_r4_gui_audit/000_design_read.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# 000 — Design Read and audit scope (lane R4)

Lane R4 of the release-readiness train audits the dashboard GUI changes merged since
v2.77.0 and fixes what it finds. This document records the design frame the audit
judges against, so a finding can be traced to a rule instead of a taste.

## Design Read

```yaml
---
name: opencodex dashboard (governed by gui/design-system/)
colors:
primary: "var(--text) #0d0d0d / #ececec"
accent: "var(--accent) — primary action and focus only"
background: "var(--bg) #ffffff / #212121, rail var(--rail) #f9f9f9 / #171717"
typography:
heading: { fontFamily: var(--font-ui), fontSize: 20px (text-title) }
body: { fontFamily: var(--font-ui), fontSize: 14px (text-body), controls 13px }
iconography:
system: "in-repo icons.tsx (stroke set)"
weight: "regular; filled/pressed state for the current choice"
domain: "provider brand marks (provider-icons.ts)"
---
```

Reading this as: a dense, repeated-work admin console for developers who run a local
proxy, in the quiet neutral language of the ChatGPT/Codex desktop apps it sits beside.
The governing design system (`gui/design-system/`, tokens in `gui/src/styles.css`)
overrides any taste call; the audit checks conformance and objective UX, not style.

Do's: one obvious row per destination, the active row always lit, every control
reachable by keyboard with a visible focus ring, copy that fits in all eleven locales.
Don'ts: emoji as UI, clipped labels, a control whose state is only conveyed by color,
dead ends on empty/error states, new visual vocabulary that the design system lacks.

```
DESIGN_VARIANCE: 3
MOTION_INTENSITY: 2
Product density profile: D5 (dense SaaS/admin, developer audience)
Reasoning: an operator console used repeatedly; clarity and density beat expression.
```

Concept generation (UX-CONCEPT-GEN-01) is skipped: this is an audit of an existing
utility surface governed by a design system, which the rule exempts.

## Scope

| PR | Surface | Stated intent to verify |
|---|---|---|
| #6579 | sidebar foot | three-icon theme switch (aria-pressed, tooltip/name); shares a row with the zoom stepper on macOS/Linux desktop; "Theme" label row in browser/Windows; alignment with language icon and orbs |
| #6593 | sidebar, Connect, Claude, Usage & Logs, Remote Link | eight rows; row lights for every owned page; section switcher (named nav, aria-current, arrow/Home/End); Connect tab order; Claude without sub-tabs; #claude/account and #claude/settings redirects; Codex Set labelled Codex |
| #6596 | Connect → Claude, Connect → Claude Desktop | one-page Claude Code settings with sticky Save bar (dirty state, Revert/Save); Desktop Models card (default and quick task model), folded Advanced lanes. Claude Desktop is owned by lane R3: findings are reported, not fixed here |
| #6522 | Connect → DSH | plan-path allowlist and semantics text in all locales |
| #6597 | Connect → file clients | missing-store remedy; audited only after it lands |

Checks per surface: intent coverage; IA and reachability (every route, active state,
collapsed and expanded sidebar, drawer at narrow widths, keyboard order, aria names);
all eleven locales (missing keys, untranslated English, truncation in de/pt/ru/fr);
light, dark and system themes; zoom 80–150% and widths 1440/1024/768/390; empty,
loading and error states on changed pages; no emoji as UI; pages that moved still work.

## Method

The built dashboard (`bun run build:gui`) is served by an isolated proxy whose HOME,
OPENCODEX_HOME, CODEX_HOME, CODEX_SQLITE_HOME and XDG_* roots live under one
`mktemp -d` directory, on a random port, after verifying the resolved state roots.
Screenshots come from headless Chrome driven by a scratch Playwright script outside the
repository. No test runner is used (owner instruction for this train).
41 changes: 41 additions & 0 deletions devlog/_plan/261005_r4_gui_audit/010_work_phases.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# 010 — Work-phase plan

## wp1 — audit (this cycle, docs and evidence only)

Parallel read-only design audits, one per surface, each returning findings with
severity, `path:line`, and screenshot path:

1. Sidebar IA and foot controls (#6579, #6593 rows): routes, active state, collapse,
drawer, keyboard order, aria, desktop UA variants, zoom, widths (including the
759/760/761px drawer boundary and short window heights, where the foot must stay
reachable per `gui/design-system/components.md`), contrast, reduced motion, locales.
Filled selection applies to the theme switch only; nav icons stay outline.
2. Connect tabs and the one-page Claude Code settings (#6593, #6596): tab strip,
Save bar states (dirty draft across tab hops, refresh and remount, the immediate
switches, in-flight edits, failed save, Revert, sticky bar overlap with content),
empty/error states, disabled controls, console errors, widths, themes, locales.
3. Section switcher, legacy redirects and deep links (#6593): Usage & Logs, Remote Link,
`#claude/account` landing on Anthropic's Accounts tab while plain Providers keeps the
whole workspace, `#claude/settings`, protocol deep links, focus recovery when Remote
Workspace drops out of the switcher.
4. Locale completeness and truncation for every key these PRs added or changed
(static diff of the eleven catalogs plus render in long locales), including #6522.
5a. DSH (#6522): rendered integration panel, `integration-api.ts` plan-path allowlist
accepting the profile patch path and the legacy path, and the semantics text.
5. Claude Desktop (#6596): report-only, for lane R3.
6. Docs-site pages these PRs changed versus the shipped UI.

Main synthesizes findings into `020_findings.md` (P0–P3, evidence), deduplicated and
verified against the source before any fix.

## wp2 — fixes

P0–P2 findings outside R3's files and outside #6597's files are fixed on
`codex/r4-gui-audit` (or split by surface if they are independent), with before/after
screenshots from the same sandbox. Static gates: `bun run typecheck`, `bun run lint:gui`,
`bun run build:gui`, `bun scripts/file-size-ratchet.ts`, `bun run structure:check`,
`bun run privacy:scan`. Merge evidence is hosted CI plus a full-matrix dispatch at the
exact head. Locale catalogs only gain keys (coordinator rule for this train): a clipped label is
fixed by layout or by a new, shorter key; a wrong existing translation is reported
rather than edited. P3s and R3 findings are reported to the
coordinator.
52 changes: 52 additions & 0 deletions devlog/_plan/261005_r4_gui_audit/020_findings.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# 020 — Findings (wp1)

Audit of #6579, #6593, #6596 and the GUI part of #6522 against the Design Read in
`000_design_read.md`. Evidence screenshots live in the lane's scratch folder
(`.tmp/r4/<auditor>/…`, not committed); the PRs carry the before/after images.
#6597 is still open and is audited after it lands.

Severity scale: P0 broken flow or data loss; P1 stated intent missing, unreachable
route, or a11y blocker; P2 visible defect a user hits; P3 polish.

## Fixed in the GUI PR (codex/r4-gui-audit)

| ID | Sev | Finding | Cause | Evidence |
|---|---|---|---|---|
| F1 | P2 | In a short window (about 320 CSS px tall, e.g. 1280×480 at 150% desktop zoom) the sidebar nav shrinks to its 8px padding and all eight rows disappear; a focused row shows only a clipped slice. The auditor rated it P1; lowered to P2 because it needs an unusually short window, and the drawer layout below 760px wide is unaffected. | `gui/src/styles/sidebar-zoom.css:48` lets the nav shrink to `min-height: 0` while brand and foot keep their height | a1/shots/short-focused-nav.png, short-native-equivalent.png |
| F2 | P2 | Claude → Web search / Vision sidecar override rows: at 768px (French) the two controls cover the setting title and squeeze the description to a word per line; at 390px the model input runs past the card edge and is clipped. | `gui/src/pages/claude-code-sections.tsx:165` unwrapping control row, `:198` fixed 210px input, shared `.setting-row` stacks only below 760px viewport | a2/shots/fr-768-sidecar-overlap.png, sidecar-mobile.png |
| F3 | P2 | A failed Save on the one-page Claude settings shows its error only at the top of the page; at the bottom, where Save was clicked, the sticky bar looks unchanged. | `gui/src/pages/ClaudeCode.tsx:419` renders status above all sections only | a2/shots/failed-save-bottom.png, failed-save-top.png |
| F4 | P3 (cheap, fixed with F3) | German Save-bar status slides under Revert at 390px and 150% zoom. | `gui/src/styles-claudecode-workspace.css:187` lets the status shrink to 0 instead of wrapping | a2/shots/de-390-zoom150-dirty.png |
| F5 | P2 | Cold-loading a legacy `#debug` or `#debug/…` bookmark lands on `#logs/debug` with the Logs tab selected instead of Debug. | `gui/src/pages/logs-tab-keydown.ts:6` reads the raw hash before the passive legacy rewrite | a3/shots/debug.png vs debug-canonical.png |

## Fixed in the docs PR (codex/r4-docs-sync)

| ID | Sev | Finding | Cause |
|---|---|---|---|
| D1 | P2 | DSH guide still names the legacy `llm-pi-ai.providers.opencodex` mapping as the owned path; the dashboard (correctly) names the `[id=llm-pi-ai].config.providers.opencodex` row in the Desktop profile patch. | `docs-site/src/content/docs/guides/integrations.md:86`, `:286`, and the seven translations |
| D2 | P2 | Guides send users to an "Integrations" tab; the sidebar row and page are called Connect since #6593. | `guides/integrations.md:6`, `:452`, `guides/web-dashboard.md:264`, translations |
| D3 | P2 | The Claude guide's GUI section lists the old switch-first order; the seven translations also omit the Save/Revert/dirty contract and that the connection switch applies immediately. | `guides/claude-code.md:921`; `fr:693`, `ja:551`, `ko:614`, `ru:583`, `tr:794`, `zh-cn:520`, `zh-tw:596` |

## Reported, not fixed

| ID | Sev | Finding | Owner / reason |
|---|---|---|---|
| R3-1 | P2 | Claude Desktop Models card: a long unavailable stored choice (route plus "(unavailable)") makes the role select overflow the card at 390px (chevron and suffix clipped) and squeezes the label column to ~6px at 768px. Cause `gui/src/pages/ClaudeDesktop.tsx:517`, `:682`, `:700`; `gui/src/styles/claude-page.css:56`. Evidence a5/long-stored/long-stored-de-390.png, -768.png | lane R3 owns ClaudeDesktop.tsx and its styles |
| K1 | P3 | Mobile drawer: Tab past the last control (or Shift+Tab on entry) leaves the page for browser chrome before cycling back; the background is correctly inert. Pre-existing, not from these PRs. | known issue |
| K2 | P3 | While Save is in flight the bar keeps saying "Unsaved changes" with disabled buttons; no pending label. | known issue |
| K3 | P3 | Connect's 25-tab strip wraps to seven rows at 390px, pushing content below the fold. Pre-existing; grew by one tab in #6593. | known issue |
| K4 | P3 | Translated web-dashboard and desktop-app guides lack the new English "Dashboard layout" and "Zoom" sections (coverage gap; nothing contradicts the UI). | known issue |

## Checked and clean

Eight rows navigate and light for every owned page (Startup lights none); collapse and
expand; drawer switches at 760/761px, Escape and inert work; theme switch names,
`aria-pressed`, alignment with the globe and orbs; mac/Linux display row vs browser
"Theme" row in nine locales; reduced motion; section switcher order, named nav,
`aria-current`, arrow/Home/End, focus survival and Remote Workspace drop-out; every
legacy redirect except F5, including `#claude/account` → Anthropic Accounts while
plain Providers keeps the workspace; Connect tab order; Claude section order, draft
across tab hops, Revert, reload, loading/error/empty states; all 26 changed keys in
eleven catalogs with matching placeholders, no removed key still referenced, no
clipping in seven long locales at 1440/1024; DSH plan-path allowlist is exact
(`integration-api.ts:262`, `:273`, `:285`); design-system notes match the code.
No emoji used as UI on the audited surfaces. No console errors in roughly 250 renders.
71 changes: 71 additions & 0 deletions devlog/_plan/261005_r4_gui_audit/030_fix_plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# 030 — wp2 fix plan (diff level)

Two PRs to `dev`, independent of each other.

## PR A — `codex/r4-gui-audit` (GUI)

F1 `gui/src/styles/sidebar-zoom.css` (desktop layout block, `@media (min-width: 761px)`):
keep the nav as the scroll area in normal windows, but floor it so it can never collapse
below about three rows, and let the sidebar itself scroll when brand + three rows + foot
do not fit:

```css
@media (min-width: 761px) {
.sidebar { overflow-y: auto; }
.sidebar nav { flex: 1 1 auto; min-height: 7.5rem; overflow-y: auto; padding: 4px; margin: -4px; }
}
```
Update the comment above the block. Check collapsed rail and drawer are unaffected.
Audit residuals folded: the sidebar only scrolls as a whole in short windows
(`max-height` media query), with `scroll-padding-top` so a focused row is not hidden
under the fixed macOS title strip (`gui/src/components/app-titlebar.css:38`); the
language dropdown (`portal={false}`, `gui/src/App.tsx:493`) is rendered open in a
short window to confirm it is not clipped.

F2 `gui/src/pages/claude-code-sections.tsx` sidecar rows: replace the inline styles
(`alignItems`, `flex: 1`, `display:flex; gap:8`, `minWidth: 210`) with a
`claudecode-sidecar-row` class; rules go into `gui/src/styles-claudecode-workspace.css`:

```css
.claudecode-sidecar-row { flex-wrap: wrap; align-items: flex-start; }
.claudecode-sidecar-row .setting-copy { flex: 1 1 16rem; }
.claudecode-sidecar-row .setting-controls { display: flex; flex-wrap: wrap; gap: var(--space-2); flex: 0 1 auto; min-width: 0; max-width: 100%; }
.claudecode-sidecar-row .setting-controls .input { flex: 1 1 13rem; min-width: 0; }
```
The copy keeps a 16rem basis, so when copy and controls do not fit on one line the
controls wrap below the copy instead of crushing it; inside, the model input wraps under
the backend select and fills the width.

F3 + F4 `gui/src/pages/ClaudeCode.tsx` and `gui/src/styles-claudecode-workspace.css`:
a failed Save reports in the Save bar, where the user clicked. New state `saveError`
(string). `save()` clears it on start and on success; on failure it sets it (same
message as today) instead of the page-top status. `revert()` clears it. The bar renders
`<span className="ccw-savebar-error" role="alert">` as its first child when set; the
`.ccw-savebar-state` span keeps its current text so existing selectors hold. CSS:
`.ccw-savebar-error { flex: 1 1 100%; color: var(--red); font-size: var(--text-label); }`
and `.ccw-savebar-state { flex: 1 1 auto; }` so the status wraps to its own line instead
of sliding under Revert. No new locale keys.

F5 `gui/src/pages/logs-tab-keydown.ts`: `readTabFromHash` compares
`canonicalHashPath(window.location.hash)` (from `../app-routing`) with `logs/debug`.
Add the legacy cases (`#debug`, `#debug/legacy` → debug) to the existing
`readTabFromHash` test in `gui/tests/logs-tab-keydown.test.ts` (no new test file).

Gates: `bun run typecheck`, `bun run lint:gui`, `bun run build:gui`,
`bun scripts/file-size-ratchet.ts`, `bun run privacy:scan`; before/after renders of
F1–F5 from the sandbox. Hosted CI and a full-matrix dispatch at the exact head.

## PR B — `codex/r4-docs-sync` (docs-site), built in a /tmp worktree

D1 `docs-site/src/content/docs/guides/integrations.md:86`, `:286` and the seven
translations: DSH 0.1.7+ owns the `[id=llm-pi-ai].config.providers.opencodex` row of
`$DSH_HOME/profiles/desktop/cordis.patch.yml`; `llm-pi-ai.providers.opencodex` in
`settings.yaml` is the fallback when no Desktop profile exists.
D2 visible navigation reads **Connect** (and **Connect → API Keys**) in
`guides/integrations.md:6`, `:117`, `:252`, `:452`, `:612`, `:748` (Integrations → Hermes,
Integrations → Factory Droid, …), `guides/web-dashboard.md:264` and translations,
keeping "integrations" as the feature name and the hashes unchanged.
D3 `guides/claude-code.md:921` GUI list in the current section order; the seven
translations gain the one-page / dirty / Revert / Save explanation and the rule that the
connection switch applies immediately and Save never changes it.
Gates: `bun run privacy:scan`, docs build if the docs workspace builds offline.
7 changes: 6 additions & 1 deletion gui/src/pages/ClaudeCode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ export default function ClaudeCode({ apiBase, active = true }: { apiBase: string
() => cached ? { draft: cached, baseline: cached, adoptNextRead: false } : null,
);
const [saving, setSaving] = useState(false);
const [saveError, setSaveError] = useState("");
const [status, setStatus] = useState("");
const [ok, setOk] = useState(false);
/*
Expand Down Expand Up @@ -329,6 +330,7 @@ export default function ClaudeCode({ apiBase, active = true }: { apiBase: string
if (!state || !edit || saving) return;
const submitted = edit.draft;
setStatus("");
setSaveError("");
setSaving(true);
setEdit(current => current && { ...current, savePending: true });
// Reads already in flight predate this Save; none of them may reach the draft or cache.
Expand Down Expand Up @@ -356,19 +358,21 @@ export default function ClaudeCode({ apiBase, active = true }: { apiBase: string
// The submitted draft is what the server now holds; edits made meanwhile stay dirty.
setEdit(current => acknowledgeSave(current, submitted));
setOk(true);
setSaveError("");
setStatus(t("claude.saved"));
codeResource.refresh();
} catch (error) {
setEdit(current => current && { ...current, savePending: false });
setOk(false);
setStatus(error instanceof Error && error.message ? error.message : t("claude.networkError"));
setSaveError(error instanceof Error && error.message ? error.message : t("claude.networkError"));
} finally {
setSaving(false);
}
};

const revert = () => {
setStatus("");
setSaveError("");
setEdit(current => current && {
draft: { state: revertEditable(current.draft.state, current.baseline.state), rows: current.baseline.rows },
baseline: current.baseline,
Expand Down Expand Up @@ -477,6 +481,7 @@ export default function ClaudeCode({ apiBase, active = true }: { apiBase: string
</div>

<div className="ccw-savebar" role="region" aria-label={t("claude.saveBar.label")}>
{saveError && <span className="ccw-savebar-error" role="alert">{saveError}</span>}
<span className={`ccw-savebar-state${dirty ? " dirty" : ""}`} aria-live="polite" aria-atomic="true">
{dirty ? t("claude.saveBar.dirty") : t("claude.saveBar.clean")}
</span>
Expand Down
7 changes: 3 additions & 4 deletions gui/src/pages/claude-code-sections.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -157,12 +157,12 @@ export function ClaudeCodeSettingsCard({
const hintKey = key === "webSearchSidecar" ? "claude.webSearchSidecarHint" : "claude.visionSidecarHint";
const listId = `claude-sidecar-models-${key}`;
return (
<div className="setting-row" key={key} style={{ alignItems: "flex-start" }}>
<div className="setting-label setting-copy" style={{ flex: 1 }}>
<div className="setting-row claudecode-sidecar-row" key={key}>
<div className="setting-label setting-copy">
<span className="title">{t(titleKey)}</span>
<span className="desc">{t(hintKey)}</span>
</div>
<div className="setting-controls" style={{ display: "flex", gap: 8 }}>
<div className="setting-controls">
<Select
value={sidecarSelectValue(override)}
options={[
Expand Down Expand Up @@ -195,7 +195,6 @@ export function ClaudeCodeSettingsCard({
disabled={!override}
list={override ? listId : undefined}
aria-label={t("dash.sidecarModel")}
style={{ minWidth: 210 }}
autoComplete="off"
/>
{override && (
Expand Down
3 changes: 2 additions & 1 deletion gui/src/pages/logs-tab-keydown.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import type { KeyboardEvent } from "react";
import { canonicalHashPath } from "../app-routing";

export type LogsTab = "logs" | "debug";

export function readTabFromHash(): LogsTab {
return window.location.hash.replace(/^#\/?/, "") === "logs/debug" ? "debug" : "logs";
return canonicalHashPath(window.location.hash) === "logs/debug" ? "debug" : "logs";
}

export function selectLogsTab(next: LogsTab) {
Expand Down
Loading
Loading