Skip to content

feat(settings): workspace settings as a drill-down with per-page save - #232

Merged
juacker merged 12 commits into
mainfrom
feat/settings-shell
Oct 16, 2026
Merged

juacker merged 12 commits into
mainfrom
feat/settings-shell

Conversation

@juacker

@juacker juacker commented Oct 16, 2026

Copy link
Copy Markdown
Collaborator

PR 1 of the settings redesign (plans/settings-redesign.md in the manager workspace). Shell only: page contents, SkillPicker, MCP widget, backend and the app-level Settings modal are untouched.

What changes

  • Window: the workspace settings overlay sits 16 px from the window edge (24 px at ≥1600 px) instead of a fixed 1040 px box. Form pages keep a 720 px reading column; the index uses 880 px.
  • Navigation: the modal opens on an index — General, Schedule (summary: "every 2 h · paused" / cron / Off), Team ("N crew"), one row per agent ("3 skills · 1 MCP · provider · disabled"), "+ Add to crew". A row slides its page into the full pane with a breadcrumb; focus moves to the page heading on entry and back to the row on return; ↑/↓ move between rows.
  • Escape: answers Stay if the leave prompt is up → lets a focused search field with text keep the key → goes back one level → closes only at the index.
  • Save: each page saves on its own via a sticky "Unsaved changes · Discard · Save" bar, shown only when dirty or after a failed save. Leaving a dirty page (back, close, click outside) asks Save / Discard / Stay in the same bar.
  • Gear icon opens the index; the agent drawer's Edit still opens the agent page directly.

Deleted

Sidebar + NavItem dirty dots, the keep-visited-pages-mounted map and its helpers, the validate-every-dirty-section-then-submit loop (never atomic), the window.confirm on close. createdAgentId stays: a page now remains open after save, so creating the Main and saving again would otherwise create a second agent.

Size

Production +435/−404, tests +161/−6 (7 files). Most churn is WorkspaceSettingsModal.tsx rewritten.

Tests

8 new cases (index→page focus round trip, Esc semantics at root/page/search field, bar only when dirty + Discard resets, leave prompt Stay/Esc/Discard/Save, saving one page writes only that page, failed validation keeps page open). Five deliberate breakages (no leave guard, no search check, bar always shown, no focus move, Esc always closing) each failed at least one test.

Not verified

No app run: layout/slide-in look, whether Esc natively clears a WebKitGTK search field, runtime skill counts. Known rough edges: after creating the Main from "Main — set up" the page keeps that title until you go back; removing a crew member returns to the index (was Team).

Code review in progress; fixes will follow as commits. Draft — owner merges.

Near-full-window overlay; the root lists General/Schedule/Team and one row
per agent with a summary, and a page fills the pane behind a breadcrumb.
Each page saves on its own through a sticky bar shown only when dirty;
leaving a dirty page asks Save / Discard / Stay. Escape goes back one level
and closes only at the root; a search field with text keeps it.

Drops the sidebar, the keep-visited-pages-mounted map, the per-section
dirty dots and the validate-all-then-submit-all loop. The gear now opens
the index; the drawer's Edit still deep-links to the agent page.
- Team and crew-assignment pages expose a SectionHandle and report dirty,
  so leaving them prompts like every other page; their inline Save buttons
  are gone (the sticky bar is the one save path). Remove-from-crew stays an
  immediate action.
- Creating the Main moves the page to the new agent (submit returns
  createdId), so Discard + Save can no longer create a second one; the
  createdAgentId workaround is deleted. The Main editor is chosen by the
  roster's isDefault, not a possibly stale defaultWorkspaceAgentId.
- Focus: the first index row when the modal opens at the index or the
  origin row is gone; Stay (and a failed Save from the prompt) returns
  focus to what triggered the leave; Discard focuses the page heading.
- Tests: overlay click on a dirty page, failed Save from the prompt, dirty
  Team/crew pages, create Main -> Discard -> Save, focus fallbacks.
@juacker

juacker commented Oct 16, 2026

Copy link
Copy Markdown
Collaborator Author

59efb5d8d — fixes from review r1:

  • M1 Team and crew-member pages now use the per-page save model (page handle + dirty reporting, inline Save buttons removed). "Remove from crew" stays immediate. "Add to crew" keeps its click-to-add + Undo flow (it is a picker, not a form).
  • M2 Creating the Main returns the new id and the page switches to it; createdAgentId workaround deleted. Main editor chosen from the agent list (isDefault), not from details.defaultWorkspaceAgentId.
  • m1 Focus: first index row on open / when the origin row is gone; Stay, failed Save and Discard return focus to the trigger or page heading.
  • m2 Tests: overlay click on dirty page, failed Save from prompt, dirty Team/crew pages, create → Discard → Save creates once, focus cases. Seven mutations each fail exactly one test.

Delta vs 85b77d333: production +142/−87, tests +208/−55. Lint, tsc, vitest (85) clean.

…s after Save

A create now moves to the new agent's page before awaiting onChanged, so a
Save during the refetch updates it instead of attempting a second create,
and Back/close is no longer pulled back afterwards. A non-create save
focuses the page heading, since the save bar unmounts with its button.

Tests cover both, plus failed Team and crew saves.
@juacker

juacker commented Oct 16, 2026

Copy link
Copy Markdown
Collaborator Author

5bb0fc41a — review r2 minors: page switches to the created Main before the refetch (no second-create gap); focus moves to the page heading after a non-create Save; failed Team/crew saves now tested (ok:false, page stays dirty). Production +7/−4, tests +63/−2. Review r2 verdict: production_quality (reviews/settings-shell-review-r2.md in the manager workspace). Remaining open check: visual/runtime behaviour in the real webview — owner testing.

The workspace settings modal becomes a fixed-size window (1200x800, or the
viewport less 24 px when smaller) with a sidebar that always lists every page — General,
Schedule, Team, the Main, each crew member, Add to crew — instead of a
root index you drill into and back out of. Pages are laid out for the
window (two columns that share its height) rather than a 720 px column
that scrolls.

Kept from the previous iteration: the per-page draft and save bar,
Save / Discard / Stay on any way out of a dirty page (now including a
sidebar click), the create-then-switch for the Main, focus on the page
title after Save.

Changed: with no index level, Escape goes innermost first — answer the
leave question, let a search field clear, leave a page's drill-in view
(new optional SectionHandle.back), then close. Sidebar navigation keeps
focus on the sidebar item.

Fixes Stay never receiving focus: React reused the Discard button's
node for Stay, so its autoFocus did not run.

General and Schedule move to WorkspacePages.tsx; shared controls (page
layouts, panels, switch, path-grant and prefix editors) to
SettingsForm.module.css and ListEditors.tsx, used by Team and crew too.
The agent form (the workspace's Main, and shared definitions in the
library) is rebuilt as an overview that fits the window, with drill-in
views for what does not:

- Runs on: a single select of enabled connections labelled like the
  chat bar. Picking writes a one-element list; a legacy multi-entry
  list is left as is until the user picks, and the hint says the extra
  entries are never used. A gone or disabled connection is shown as
  such. No backend change.
- MCP servers: a dropdown of checkboxes whose trigger names what is on.
  It joins the overlay stack, so Escape closes it and not the window.
- Skills, sources first: one row per source with its selected count;
  opening one shows only that source's skills as a compact tile grid,
  with the focused skill's description in a fixed strip. A search on
  the overview finds skills across sources.
- Local access: a summary on the overview, edited in its own view
  (paths | shell, web, D-Bus). The blocked-prefix list now shows under
  Full as well as Restricted, since local.rs applies it in both.

back() on the handle leaves a drill-in view, and focus returns to the
control that opened it. The form no longer draws its own heading; the
window title and the library editor already do.
- The save bar moves into the page header, so no footer height is
  reserved while nothing is unsaved, and the skill search moves into the
  Skills panel header. A 9-source overview now fits a 652 px window.
- Escape inside a drill-in view of the agent library editor goes back
  instead of closing global Settings and dropping the draft.
- The MCP popover renders inside the dialog, after its trigger: Tab past
  the end closes it, Shift+Tab reaches the trigger.
- The agent form keeps its skeleton until deps load, so no false
  "connection is gone" or empty-list warnings flash.
- Select all / Clear in a source act on the filtered skills.
- Accurate legacy-connection hint, subtitle and save-error tooltips,
  useReportDirty instead of a copy, unused `details` prop and CSS removed.
- Tests: legacy connection list kept on save, dirty tracking in both
  drill-in views, filtered Select all, deps loading, library Escape,
  MCP Tab order.
@juacker

juacker commented Oct 16, 2026

Copy link
Copy Markdown
Collaborator Author

Settings redesigned from scratch after owner feedback (design: page sidebar, fixed-size dialog, agent page as a one-screen overview, skills browsed source-first, MCP dropdown with checkboxes, single 'Runs on' model choice). Commits a89265a, 8382f71, 6164daf. Review v2-r1 needs_fixes → all 4 majors fixed in 6164daf (fits at 1366×768 per Chromium measurement; multi-connection save test; library Esc steps back; MCP popover focus stays in dialog). Settings vitest 119 pass, lint + tsc clean. Not verified in WebKitGTK; review r2 in progress.

- Schedule: re-sync the draft on the saved schedule's value, so the 5 s
  details poll, Run now and Pause no longer discard an unsaved edit.
- Team: a failed policy load shows only the error and refuses to save,
  instead of an empty form that would overwrite the stored policy.
- Library editor: focusable root keeps Escape inside it; Escape in a
  search field with text clears it instead of closing Settings.
- Layout: Skills header search and count shrink; the save bar shrinks and
  ellipsizes before the page title does; the asking state colours the
  whole bar border.
- Test that the overview's Clear all keeps unknown skill ids.
@juacker

juacker commented Oct 16, 2026

Copy link
Copy Markdown
Collaborator Author

c280aae — review v2-r2 fixes: Schedule draft no longer wiped by the 5 s details poll / Run now / Pause (reset keyed on value; pre-existing bug); Team page no longer shows an editable empty form when the policy fails to load (pre-existing, could overwrite stored settings); skills header shrinks at 961–1130 px; library editor keeps focus for Esc; amber leave bar; title min width; Clear-all test. Prod +49/−21, tests +55. Settings vitest 122 pass, lint + tsc clean. Not verified in WebKitGTK.

The app Settings dialog had its own older shell (SettingsModal.module.css):
980×760, a 14 px sidebar, a blurred backdrop and an unbounded content
column. Workspace settings is 1200×800 with a 13 px sidebar. Both now use
one stylesheet, SettingsShell.module.css, so they are the same window.

The library agent editor already used the workspace agent form, but sat in
a content-sized column under a title, a face row and a footer, so the form
never got a height to fill and the pane scrolled (479 px over at 1249×995).
The editor now fills the pane: Save/Archive share the breadcrumb row and
the description sits beside the face picker. Measured in headless Chromium
at 1249×995: no scroll; at a 1366×768 screen it still scrolls by 106 px.
Four things repainted already-shown content on a navigation:

- The page area had `animation: fadeIn` and is keyed by page, so every
  page switch, Discard and post-create hop faded the whole pane in from
  opacity 0. The fade is gone; the window's open animation stays.
- The modal stayed mounted while closed and reset its page in an effect.
  That effect runs at default priority after the open commit, so a reopen
  painted the page it was closed on (and started its fetch) before the
  requested one. The window now mounts per opening, keyed by the requested
  page, and the reset effect is deleted.
- Page skeletons replaced the pane for the few ms a local load takes, then
  gave way to the real layout. They now stay invisible for 300 ms.
- An agent's sidebar row grew from 36 to 48 px when its model name arrived
  with the connections, moving every row below it. The line is reserved.

Also, the app Settings Agents tab said "No agents yet" on every visit until
the library answered; the gallery now waits for that first answer.
- Pages no longer repeat the window header's title (Agents, Skills, MCP
  Servers, Applications, Appearance); About's CLAI heading is an h3.
- App Settings mounts only while open, keyed by the requested tab and
  agent, like workspace settings; the render-phase reset state is gone.
  Focus starts on the active sidebar item.
- The Agents tab shows the delayed skeleton while the library is first
  read, and only the error with Retry if that read fails.
- The agent editor zeroes the form's page padding through a custom
  property instead of a descendant selector; its back link and title are
  no longer a breadcrumb nav.
- Drop the unused shell copy of .navAdd/.navPlaceholder and correct the
  .body scrolling comment.
The owner sees a dialog flicker over and under the window behind it as it
opens, in WebKitGTK only, and only during the open. Nothing in the stacking
changes then: modal overlays sit at z-index 10000+, portaled to <body>,
and nothing else in the app is above 1000. What does change is
compositing: the backdrop fades its opacity in, the dialog animates
opacity and transform, and several backdrops also blur what is behind
them. Each one makes WebKit promote the layer for the animation and demote
it after.

Every modal overlay now appears at once with no backdrop-filter: the
settings shell, the MCP/provider form modal, ConfirmDialog,
ProgressDialog, CommandHelpModal and the MCP server selector. The
keyframes that nothing uses any more are deleted.
@juacker
juacker marked this pull request as ready for review October 16, 2026 23:18
@juacker
juacker merged commit 95162c6 into main Oct 16, 2026
3 of 4 checks passed
@juacker
juacker deleted the feat/settings-shell branch October 16, 2026 23:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant