Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
7c3770a
docs(devlog): plan Claude settings single page and Desktop model roles
lidge-jun Oct 4, 2026
fcdaa3a
feat(gui): lay Claude Code settings out as one page with a sticky sav…
lidge-jun Oct 4, 2026
033f62c
fix(gui): make a successful Claude Code save the baseline before its …
lidge-jun Oct 4, 2026
6cffe47
feat(gui): lead Claude Desktop with default and quick task models, ti…
lidge-jun Oct 4, 2026
7ee8aca
fix(gui): keep Claude Desktop import and export reachable with no models
lidge-jun Oct 4, 2026
72cc4bc
docs: describe the one-page Claude settings and Desktop model roles
lidge-jun Oct 4, 2026
bbdf40d
perf(gui): single pass for Desktop role defaults, toSorted for the li…
lidge-jun Oct 4, 2026
7628b12
fix(gui): show the normalized interception rows once a Claude Code sa…
lidge-jun Oct 4, 2026
fd1a214
refactor(gui): build the Claude Code save body from the submitted draft
lidge-jun Oct 4, 2026
4cb265c
fix(gui): drop Claude Code reads that a successful write has superseded
lidge-jun Oct 4, 2026
4792a53
fix(gui): keep reads that overlap a Claude Code save out of the sessi…
lidge-jun Oct 4, 2026
945cb7d
fix(gui): publish the Claude Code save cache from the latest confirme…
lidge-jun Oct 4, 2026
794c518
fix(gui): show a committed Claude switch even when its reread fails
lidge-jun Oct 4, 2026
0279e0b
fix(gui): share the Claude Code write epoch across mounts
lidge-jun Oct 4, 2026
96baf24
fix(gui): build the Claude Code save cache from the shared session copy
lidge-jun Oct 4, 2026
8f3b06a
fix(gui): fold shared Claude Code reads into the draft of the page on…
lidge-jun Oct 4, 2026
abd49b4
fix(gui): deliver Claude switch acknowledgements to the page on screen
lidge-jun Oct 4, 2026
56a530b
fix(gui): deliver Claude Code save confirmations to the page on screen
lidge-jun Oct 4, 2026
43cfeef
fix(gui): keep edits made while a Claude Code save is out
lidge-jun Oct 4, 2026
97ccfbe
docs(devlog): record the Claude Code save hardening from review
lidge-jun Oct 4, 2026
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
82 changes: 82 additions & 0 deletions devlog/_fin/261005_claude_settings_ux/000_plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# 261005 Claude settings UX

The Connect > Claude tab shows Claude Code settings behind a second, inner sidebar
(General, Get started, Background helper model, Model interception, Available models).
With the app sidebar beside it, the page carries two rails, and every setting is one
extra click away. The Claude Desktop tab leads with four Claude family lanes (Opus,
Fable, Sonnet, Haiku), which is an implementation detail of how Desktop's model list is
written; most users only want to pick the model Desktop opens with and the model it uses
for light background work.

The user approved two designs on 2026-10-05 from the visual drafts in
`~/.codex/visualizations/2026/10/04/01a10726-.../`:

- **A** for Claude Code: one vertical page, no inner rail, a single sticky save bar,
the Claude connection master switch kept last.
- **D1** for Claude Desktop: a Models card with Default model and Quick task model, a
compact model list with role badges, and the family lanes moved under a collapsed
Advanced disclosure.

## Loop spec

- Archetype: satisfy-spec, HOTL under cxc-loop, four work-phases.
- Goal: both designs implemented in the GUI, all locales translated, docs synced,
gates green, PR merged to `dev` with exact-head CI.
- Non-goals: backend or management API changes, the Desktop profile wire contract
(`src/claude/desktop-profile.ts`, `anthropicFamilyTier`), sidebar/navigation,
1P picker semantics, release or promotion.
- Verifier: focused gui tests, `bun run lint`, `lint:i18n`, `build` in `gui/`; root
`typecheck`, `structure:check`, `privacy:scan`, test suite per AGENTS.md; in-app
browser render of both tabs against a local QA proxy.
- Stop condition: goalplan criteria c-1..c-5 carry fresh evidence.
- Resource bounds: this worktree, the local QA proxy's own admin token, GitHub PR/CI on
the opencodex repository (merge authorized by the user). No stated token budget.
- Delegation: gpt-6.1-sol subagents for architect, audit, locale translation and review,
each with a non-overlapping file scope and no git branch operations.

## Work-phase map

| WP | Doc | Outcome | Depends on |
|----|-----|---------|------------|
| wp0 | this file + 010/020/030 | diff-level roadmap | - |
| wp1 | [010](./010_phase1_claude_code_single_page.md) | Claude Code single page (A) | wp0 |
| wp2 | [020](./020_phase2_desktop_model_roles.md) | Claude Desktop model roles (D1) | wp1 |
| wp3 | [030](./030_phase3_land.md) | gates, PR, CI, squash merge | wp2 |

## Architect consultation

- Handle `01a1080c-7273-7923-aa76-632d14af55f8` (gpt-6.1-sol, architect, read-only), 2026-10-05.
- D1 vertical flow order: ACCEPT.
- D2 explicit accepted baseline: ACCEPT. `fetchCode` also runs directly after the 1P toggle,
so resource data alone cannot be the dirty baseline. ClaudeCode keeps one combined draft
`{ state, rows }` plus an accepted baseline; a read replaces the draft only when it is clean,
otherwise it refreshes server-owned fields and keeps the editable ones (010).
- D3 canonical comparison (sorted, trimmed modelMap; sidecars via `serializeSidecarOverride`;
raw row text so blank/incomplete rows still count as a change): ACCEPT.
- D4 omit `enabled` from the ordinary Save body: ACCEPT. Verified the PUT handler only writes
`enabled` when present (`src/server/management/agent-settings-routes.ts`), so a stale Save
can no longer undo an immediate connection toggle.
- D5 save bar (Save enabled when clean, disabled while saving; Revert disabled when clean;
labelled region; separate polite status node): ACCEPT.
- D6 Desktop wording: PARTIAL. The user approved the labels "Default model" and "Quick task
model", so they stay. The descriptions name the mechanism instead of promising Desktop
behavior: Default model is listed first and sent as the Opus tier; Quick task model answers
Desktop's Haiku-tier requests.
- D7 atomic move-and-default, mutual exclusion, "Not set" only when Haiku has no
assignment: ACCEPT.
- D8 lanes stay mounted inside a closed `<details>`; summary carries warnings: ACCEPT.
- D9 tests for draft/read race and select invariants: ACCEPT.

## Outcome (2026-10-05)

Implemented as planned. Claude Code settings render as one page with a sticky Save bar;
Save no longer sends `enabled`, and a successful Save becomes the baseline before its
refresh (`gui/src/pages/claude-code-save.ts`). Claude Desktop leads with Default model and
Quick task model (`gui/src/pages/claude-desktop-roles.ts`), a compact model list, and the
family lanes under a folded Advanced disclosure; the profile wire contract is unchanged.
Reviews: architect, auditor and two code reviewers (gpt-6.1-sol); findings folded
(save-acknowledgement races, import/export with an empty catalog, stale guide navigation).
Verification: `gui` full tests 2889 pass, lint, lint:i18n, build; root typecheck,
structure:check, privacy:scan, full `bun run test`; docs-site build; in-app browser QA of both
tabs. What did not change: the subtitle on the Desktop tab still speaks of model families,
and the Quick task label rests on the Haiku tier mapping rather than observed Desktop behavior.
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# 010 Phase 1 - Claude Code single page (design A)

## Problem

`gui/src/pages/ClaudeCode.tsx` renders a `claudecode-workspace-root` grid with an
`aside.claudecode-workspace-rail` of five buttons and one visible pane chosen by
`selectedSection`. The Save button sits in the pane head and hides itself on read-only
panes. Inside Connect this is a second sidebar next to the app sidebar.

## Change

### MODIFY gui/src/pages/ClaudeCode.tsx

- DELETE `selectedSection` state, the `sections[]` array, `selected`, `sectionEditable`,
the `<aside className="claudecode-workspace-rail">` block and the `ccw-main-head` save.
- Render, in this order, inside `<div className="claudecode-workspace-shell claudecode-doc">`:
1. status / load-error notices (unchanged);
2. CLI 1P card, intercept start, 1P notices (unchanged, still first);
3. `<CcwSection id="quickstart" title={t("claude.quickstart")}>` with
`ClaudeCodeQuickstartSection` (ocx claude + manual env disclosure);
4. `<CcwSection id="settings" title={t("claude.workspace.settings")}>` with
`ClaudeCodeSettingsCard` **without** `footer`;
5. `<CcwSection id="smallFast" title={t("claude.smallFastModel")}>` with
`SmallFastModelSetting`;
6. `<CcwSection id="modelMap" title={t("claude.modelMap")} count={rows.length}>`
with `ClaudeCodeModelMapSection`;
7. `<CcwSection id="aliases" title={t("claude.aliases")} count={state.aliases.length}>`
with `ClaudeCodeAliasesSection`;
8. `<div className="card claudecode-connection-card claudecode-master-card">`
holding the existing `connectionRow` (immediate commit, last on the page);
9. sticky save bar:
```tsx
<div className="ccw-savebar" role="region" aria-label={t("common.save")}>
<span className="ccw-savebar-state" aria-live="polite">
{dirty ? t("claude.saveBar.dirty") : t("claude.saveBar.clean")}
</span>
<button className="btn btn-ghost btn-sm" disabled={!dirty} onClick={revert}>{t("claude.saveBar.revert")}</button>
<button className="btn btn-primary btn-sm" onClick={() => void save()}>{t("common.save")}</button>
</div>
```
Save stays enabled when clean (same as before; re-saving is harmless and existing
tests save without edits).
- `CcwSection` is a local component: `<section className="ccw-section" aria-labelledby>`
with `<h3 className="ccw-section-title">` and an optional `.count` chip.
- Dirty and save: see "Amendments" below; they are authoritative for draft, baseline and
the Save body (`enabled` is not sent by Save).

### NEW gui/src/pages/claude-code-save.ts

Pure `claudeCodeSaveBody(state, rows)`: trims and filters `rows` into `modelMap`, serializes
sidecars with `serializeSidecarOverride`, and returns authMode, systemEnv, fastMode,
autoContext, autoCompactWindow, injectAgents, smallFastModel, modelMap,
webSearchSidecar, visionSidecar.

### MODIFY gui/src/pages/claude-code-sections.tsx

Comments that mention "rail pane" / "ccw-main-head" now say the section heading lives
in ClaudeCode's `CcwSection`. No behavior change.

### MODIFY gui/src/styles-claudecode-workspace.css

DELETE rail/grid rules (`.claudecode-workspace-root`, `-rail*`, `-main`, `.ccw-main-*`,
`.claudecode-workspace-save*`, `.ccw-body` and the narrow-width rail override). ADD
`.claudecode-doc` (max-width ~880px, gap), `.ccw-section`, `.ccw-section-title`,
`.ccw-savebar` (position: sticky; bottom: 0; background fade from `--bg`; z-index above
cards), `.claudecode-master-card` (subtle danger border token). Tokens only from
`gui/design-system`.

### i18n (all 12 locales)

`claude.saveBar.dirty` "Unsaved changes", `claude.saveBar.clean` "No changes",
`claude.saveBar.revert` "Revert".

### Tests

- MODIFY `gui/tests/claudecode-layout.test.ts`: assert no `claudecode-workspace-rail` and no
`selectedSection`; keep the source-order test, extended so the master switch row
renders after `<ClaudeCodeAliasesSection` and the save bar is last.
- MODIFY `gui/tests/claudecode-fetch-errors.test.tsx`: the helper-model test no longer clicks a
rail row; the combobox is on the page directly.
- NEW case (mounted) in `gui/tests/claudecode-fetch-errors.test.tsx` or a sibling: editing a
setting shows "Unsaved changes"; Revert restores the server value; the master switch is
the last `.setting-row` before the save bar.

## Verification

`cd gui && bun test tests/claudecode-layout.test.ts tests/claudecode-fetch-errors.test.tsx tests/claude-code-*.test.tsx tests/claude-page.test.tsx && bun run lint:i18n && bun run build`,
then render `#claude` in the in-app browser against the QA proxy (desktop and ~760px width).

## Amendments after architect consultation

- Replace `draftState`/`draftRows`/`hasDraftRows` with ONE state object
`edit: { draft: {state, rows}, baseline: {state, rows} } | null`, so a read updates draft and
baseline in the same functional update (no ref needed, no torn pair).
- `claude-code-save.ts` exports: `EDITABLE_KEYS`, `claudeCodeSaveBody(state, rows)` (no `enabled`;
modelMap trimmed, last duplicate wins, keys sorted), `isClaudeCodeDraftDirty(draft, baseline)`
(body differs OR raw row `[from, to]` list differs), `mergeServerRead(draftState, nextState)`
(server state with editable keys from the draft), `revertEditable(draftState, baseState)`.
- `fetchCode`: `setEdit(cur => !cur || !isDirty(cur.draft, cur.baseline) ? { draft: next, baseline: next }
: { draft: { state: mergeServerRead(cur.draft.state, next.state), rows: cur.draft.rows }, baseline: next })`.
Save acknowledgement (audit blocker 1): `edit` also carries `adoptNextRead: boolean`. When a
Save succeeds and the current draft still equals the submitted draft (raw rows included),
set `adoptNextRead = true`; any later edit resets it to false. `fetchCode` adopts the server
copy wholesale when the draft is clean OR `adoptNextRead` is true, so trimmed, blank or
duplicate rows normalize after a successful Save instead of staying dirty forever; edits
made during the Save keep the merge path.
- `save()` body is `claudeCodeSaveBody(state, rows)` only; a `saving` flag disables Save and
Revert while in flight. Immediate toggles merge only `enabled` / 1P fields into both draft
and baseline.
- Save bar: opaque `--bg` background, `scroll-padding-bottom` on the scroller so a focused
control is not hidden under it, status node `aria-live="polite" aria-atomic="true"`.
- NEW tests in `gui/tests/claude-code-save.test.ts` (pure) and mounted cases: dirty + revert,
a trimmed/blank/duplicate row Save that ends clean, an edit during Save that survives, and
a dirty draft surviving the 1P toggle's re-read.
- Layout test (audit blocker 2): rewrite `gui/tests/claudecode-layout.test.ts` order to
Quickstart, Settings card, SmallFastModelSetting, ModelMap, Aliases, connection row, save
bar; delete the `ccw-body`, `ccw-main-head`, `selectedSection` and `data-visible` assertions.
- Locales: add keys to every locale file `gui/src/i18n/shared.ts` registers (currently 11),
not a fixed count.
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# 020 Phase 2 - Claude Desktop model roles (design D1)

## Problem

`gui/src/pages/ClaudeDesktop.tsx` shows the four family lanes as the main body. The
backend writes every model with an `anthropicFamilyTier`, and
`renderDesktopProfile` (src/claude/desktop-profile.ts) orders the effective family
defaults first (opus, fable, sonnet, haiku). The opus default is therefore the first
model Desktop lists, and the haiku default is the model Desktop treats as its Haiku-class
model. Users need those two choices, not the tier taxonomy.

## Change

### NEW gui/src/pages/claude-desktop-roles.ts

Pure, unit-tested helpers over the existing profile shape:

```ts
export function assignFamily(profile, route, family, makeDefault): DesktopProfile
// same rules as today's moveModel: reassign the old family's default to the first
// remaining member (sorted) when the moved route was it; set the target default when
// it is null or when makeDefault is true.
export function effectiveFamilyDefaults(models, profile): Record<Family, string | null>
// stored default if it is an available member, else first available member sorted.
export function roleOptions(models, profile, exclude): string[]
// available routes, sorted by label, minus the excluded route.
```

`moveModel` in ClaudeDesktop calls `assignFamily(..., false)`; the Models card calls
`assignFamily(..., true)` with `opus` (Default model) or `haiku` (Quick task model).

### MODIFY gui/src/pages/ClaudeDesktop.tsx

After the status bar and first-party sections, before the lanes:

1. `<section className="card claude-desktop-roles">` with two `.setting-row`s using
`Select` from `../ui` (portal, align right):
- Default model: value `effectiveDefaults.opus ?? ""`; options = available routes minus
the current quick-task route; onChange -> `assignFamily(profile, route, "opus", true)`.
- Quick task model: value `effectiveDefaults.haiku ?? ""`; options = available routes
minus the default route, plus `{ value: "", label: t("claudeDesktop.roles.unset") }` only
while the haiku family is empty; onChange (non-empty) -> `assignFamily(..., "haiku", true)`.
A route can belong to one family only, so the two selects exclude each other's route.
2. `<section className="claude-desktop-list">` heading `claudeDesktop.roles.listTitle` with the
count and `claudeDesktop.roles.listHint`; Import/Export JSON buttons move here from the
toolbar. Rows (`.claude-desktop-list-row`, not `article.claude-model-card`): label,
route, context chip, 1M chip, `claudeDesktop.defaultBadge` / `claudeDesktop.roles.quickBadge`,
availability badge. Ordered default, quick, then the rest by label; first
`LANE_PAGE` rows, then `models.showMore`.
3. `<details className="claude-desktop-advanced">` (closed by default, open state kept in
component state) whose `<summary>` shows `claudeDesktop.advanced.title` plus one chip per
family: family name, its effective default or `claudeDesktop.advanced.empty`, and
`claudeDesktop.chooseDefault` when a non-empty family has no stored default. The existing
`ocx-group-stack` lanes render inside, unchanged, so drag/move/default/search keep working
and existing lane tests still find their DOM.

### CSS

ADD to the Claude Desktop stylesheet section (`gui/src/styles/claude-page.css` or the file
that owns `.claude-desktop-toolbar`): `.claude-desktop-roles`, `.claude-desktop-list`,
`.claude-desktop-list-row`, `.claude-desktop-advanced > summary`, family chips. Tokens only.

### i18n (all 12 locales)

`claudeDesktop.roles.title` "Models", `.default` "Default model", `.defaultDesc`,
`.quick` "Quick task model", `.quickDesc`, `.unset` "Not set", `.pick` "Choose a model",
`.listTitle` "Desktop model list", `.listHint` "Filled automatically from your connected
providers.", `.quickBadge` "Quick tasks"; `claudeDesktop.advanced.title` "Advanced: Claude
tier assignment", `.desc`, `.empty` "empty".

### Tests

- NEW `gui/tests/claude-desktop-roles.test.ts`: assignFamily default reassignment,
makeDefault, effective defaults with unavailable members, option exclusion.
- NEW mounted case (sibling file `gui/tests/claude-desktop-role-card.test.tsx`): picking a
Default model updates the opus default and marks the profile unsaved; lanes are inside a
closed `details.claude-desktop-advanced`.
- Register new test files in `scripts/test-layout` only if they live under root `tests/`
(gui tests are outside that layout).

## Verification

`cd gui && bun test tests/claude-desktop-*.test.ts* && bun run lint:i18n && bun run build`,
then render `#claude/desktop` in the in-app browser.

## Amendments after architect consultation

- Labels stay as approved; descriptions: Default model "Listed first in Claude Desktop and
sent as the Opus tier." Quick task model "Answers Claude Desktop's Haiku-tier requests."
Row badges: `claudeDesktop.defaultBadge` and
`claudeDesktop.roles.quickBadge`.
- `assignFamily(..., makeDefault=true)` sets the destination default even when the route is
already in that family. Aliases and applied markers are preserved.
- Select value is the STORED default (`profile.defaults.opus` / `.haiku`), falling back to the
effective default only when nothing is stored. A stored route that is unavailable is added
as its own option labelled with `claudeDesktop.unavailable`, so it shows as selected; the
Advanced summary shows the temporary-default warning and per-family counts.
- The compact list is a GUI overview (default, quick, then by label); its hint does not
claim to be Desktop's own order.
- Role changes go through the same path as `moveModel`: they update `destinations` and
announce through the polite live region.
- Locales: every locale registered in `gui/src/i18n/shared.ts` (currently 11).
- In first-party mode the card shows a one-line scope note
(`claudeDesktop.roles.gatewayScope`) because the profile only applies to gateway mode.
18 changes: 18 additions & 0 deletions devlog/_fin/261005_claude_settings_ux/030_phase3_land.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# 030 Phase 3 - land

1. `git fetch origin dev`; rebase on it if it moved; resolve conflicts in place.
2. Sync docs: `gui/design-system/components.md` (Claude tab is one page; Desktop has
Models card + Advanced lanes), `structure/gui-and-management-api.md` (owns
ClaudeDesktop.tsx), the English `docs-site` Claude guide (Desktop profile section, GUI
section, and every stale `Claude → Code/Desktop` navigation path), and the same
navigation paths in the seven translated guides so none contradicts the GUI.
3. Gates: `cd gui && bun test tests && bun run lint && bun run lint:i18n && bun run build`;
root `bun run typecheck`, `bun run structure:check`, `bun run privacy:scan`,
`bun run test` (or focused set + recorded reason per AGENTS.md);
`cd docs-site && bun install --frozen-lockfile && bun run build`.
4. Record the outcome in this unit and move it from `devlog/_plan/` to `devlog/_fin/` in the
final commit, before push.
5. Screenshots of both tabs uploaded to the `pr-assets` branch, linked by commit SHA.
6. `gh pr create --base dev` using the template (Summary, Verification, Checklist).
7. Wait for exact-head required CI; fix findings; record the maintainer integration
decision and evidence; squash merge; confirm the merge commit on `origin/dev`.
Loading
Loading