Skip to content

feat(app): widget settings view — rail, scheme tiles, scope menu (#572 part 3) - #578

Open
omridevk wants to merge 6 commits into
issue-572-settings-foundation-v2from
issue-572-settings-ui
Open

feat(app): widget settings view — rail, scheme tiles, scope menu (#572 part 3)#578
omridevk wants to merge 6 commits into
issue-572-settings-foundation-v2from
issue-572-settings-ui

Conversation

@omridevk

@omridevk omridevk commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Stacked on #592 (issue-572-settings-foundation-v2). This branch is rebased onto that PR's head and contains only the widget settings UI; review #592 first, and merge it first.

Part 3 of #572: the widget settings view. A nested settings route under the panel with a trace-rail section nav, an appearance section whose scheme preview tiles apply instantly, and a provenance badge that doubles as the scope menu. SegmentGroup lands in @conciv/ui-kit-system as an Ark segment group whose indicator slides between segments, plus a switch thumb that actually travels; both snap under reduced motion.

Adaptation to the #592 contract

The branch was originally written against the closed #574 (sqlite) foundation. The rebase dropped that foundation commit wholesale and replayed the UI work onto #592, so nothing server-side or contract-side comes from this branch any more. What changed in the UI to meet the shipped contract:

One read. settings.get returns the effective value, the controlling layer, each layer's raw value and validity, and each layer's revision. The scope badge used to open a second history query to decide whether a global value existed; it now reads layers.global.state from the same read. There is no inspect call and no reference to the old audit name. One read replaces two.

Key namespaced. scheme is appearance.scheme, and the fallback is read from the registry entry rather than hardcoded.

New shapes. SettingsRead / SettingsKeyView replace ResolvedSetting[], so the optimistic cache update rewrites the key view rather than a flat row array. The optimistic value and source are computed by the same project ?? global ?? default rule the server uses, applied to the layers the write is about to produce, instead of three hand-written expected states.

Ordinary edits always write the project layer. Previously the write followed provenance, so editing a globally-scoped value silently rewrote the global layer for every project. Now a value inherited from the global layer moves to this project when you change it: the badge flips GLOBAL to PROJECT, the footer reads "Changes save automatically to this project.", the announcement says the global setting still applies elsewhere, and the badge menu offers "Use global value" to go back. The global value is left intact.

Apply-to-all is one server call. The branch composed a global set plus a project clear client-side. It now calls settings.applyGlobally, which does both under one lock, one opId and one settings-changed event. Covered by a test that counts the wire calls.

Every write carries its expected revision, read from get's layers[scope].revision. A REVISION_CONFLICT reloads the settings and surfaces a non-blocking inline message rather than retrying over whatever the other writer stored. LAYER_UNPARSEABLE says the file has a syntax error and offers no retry until it is fixed; LOCK_TIMEOUT and the value/key errors get their own copy. Live-region announcements are worded separately from the inline copy so both surfaces are independently assertable.

Event payload. The settings-changed listener keys on the event name only and invalidates the settings query, which is idempotent, so the {opId, keys, scopes} payload change needed no listener change.

Test plumbing. bootCoreKit / bootEmbedKit forward globalSettingsDir, matching createApp's actual option name (the branch passed globalStateDir, which createApp does not accept).

No visual change: layout, rail geometry, motion, spacing and tokens are exactly as approved.

Tests

13 integration tests in packages/embed/tests/e2e/settings-panel.it.test.ts, against the prebuilt embed and a real server writing real files in temp dirs. Three are new for the contract deltas above:

  • an edit while the value comes from the global layer moves the setting to this project (and the global value survives, proven by "Use global value" still being offered)
  • applying to all projects goes through one server call, not a set plus a clear
  • a stale revision surfaces a conflict the user can retry, and never clobbers the stored value

The conflict test does not stub anything: it rewrites the outgoing request's expectedRevision at the network boundary so the real server returns a real 409.

TDD, batched. The three tests were written first and run once against the un-adapted branch: 3 failed for their own reasons (badge read GLOBAL where PROJECT was expected; zero applyGlobally calls; no conflict message), 10 passed. After the fix, 13/13. Revert-check: with only the behavioral source reverted and the tests untouched, the same 3 failed again and the other 10 stayed green.

Gates

  • TURBO_CONCURRENCY=70% pnpm turbo run typecheck --filter=@conciv/app --filter=@conciv/embed --filter=@conciv/extension-testkit --filter=@conciv/ui-kit-system --filter=@conciv/client - pass (36/36)
  • TURBO_CONCURRENCY=1 VITEST_MAX_FORKS=1 pnpm turbo run test --concurrency=1 --filter=@conciv/app --filter=@conciv/ui-kit-system --filter=@conciv/client --filter=@conciv/extension-testkit - pass (324 tests)
  • TURBO_CONCURRENCY=1 VITEST_MAX_FORKS=1 pnpm turbo run test --concurrency=1 --filter=@conciv/embed - pass (22 unit, 146 integration)
  • pnpm lint - pass (0 errors)
  • pnpm format:check - pass
  • pnpm exec fallow audit --changed-since origin/issue-572-settings-foundation-v2 --format json - verdict pass, 0 introduced
  • pnpm exec conciv-publish check-changesets --require-coverage --base origin/issue-572-settings-foundation-v2 - pass

Flagged

Resetting to the default when both layers hold a value still sends two clear calls, one per layer. Resolved (delta 15). #592 landed a settings.reset({key, expectedRevisions}) op mirroring applyGlobally. Reset now goes through that single call instead of a per-scope clear loop, covered by a test that counts the wire calls (one settings.reset, zero clear).

The panel does not surface layers[scope].path, format, parseError or the both-files-present warning. Nothing in the frozen design has a place for a file path, and adding one is a visual change. The unparseable-file case does reach the user through the write error copy.

Part 3 of #572.

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Aug 22, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 5fb61584-0b70-45f0-ab11-27fde02ac4bf

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@omridevk
omridevk force-pushed the issue-572-settings-ui branch from 8a48ee9 to f322635 Compare August 23, 2026 01:27
@omridevk
omridevk changed the base branch from issue-572-settings-foundation to issue-572-settings-foundation-v2 August 23, 2026 01:27
@omridevk
omridevk force-pushed the issue-572-settings-foundation-v2 branch from f45e68d to a4acc0b Compare August 23, 2026 07:35
omridevk and others added 5 commits August 23, 2026 10:38
…tion

Adds a SegmentGroup primitive built on Ark's segment group. Its selected
indicator is a real layout animation: zag publishes the selected item's rect
as --left/--top/--width/--height and reads --transition-duration and
--transition-timing-function back out, so the indicator slides between
segments the same way the tabs indicator does. Under prefers-reduced-motion
the duration collapses and the indicator snaps.

The switch thumb moved with `translate`, which trans-btn never listed among
its transitioned properties, so the thumb jumped. It now transitions
`translate` and snaps under reduced motion.

Refs #572

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…part 3)

A nested settings route under the panel: a layout route with a trace-rail
section nav plus an outlet, an appearance child, and an index that redirects
to it. The session rail menu opens it; the settings header carries the
two-line SETTINGS microlabel over the active section title with back and
close.

The appearance section renders the scheme setting as three SegmentGroup
preview tiles that write through settings.set the moment they change, with
no Save button. The provenance badge is the scope control: it opens an Ark
menu with apply to all projects, use global value (only when a project
override shadows a global one), and reset to default, and flashes a saved
check as it flips to its new source.

The resolved scheme now folds the stored preference over the host detection
ladder, so auto keeps following the page live while light and dark pin the
widget. A settings-changed notification on the session stream refetches the
resolved settings, so a change made in another tab or by the agent repaints
an open widget without a reload.

The section rail reuses the trace-rail geometry with a complement clip:
separate gray layers above and below the live band mean no pixel is ever
painted by both colors, arms start at the spine stroke's outer edge, and
centerlines snap to half-integers. A container query on the panel flips a
custom property that switches the rail between the vertical spine and a
horizontal underline, and every property that query re-decides lives in
styles.css rather than as a utility class, since the generated utility
layer is emitted last.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ovenance

Review round on the settings view.

Writes now follow the resolved source: editing a value that comes from the
global layer writes global and the badge stays GLOBAL, while project and
default sources write to the project. The badge menu gains "Set for this
project only" so a global value can be forked into a project override, and
the footer names the layer the next write lands in.

The view is rebuilt against the approved mockup: the provenance badge is a
small mono chip tinted per source rather than an outlined button, its menu
uses the panel's own menu recipe, the section rail carries all three rows
with their icons (Composer and Connection disabled until their settings
land) so the rail has its full geometry, and the nav label reads SETTINGS.
The tiles are a plain three-column grid with no segmented-control wrapper
and no hover fill, selection reads as the accent ring alone, and the card
sits at the top of a sunken pane with the scope line pinned to the bottom.

The container query never fired when the panel was dragged narrow: its
breakpoint sat at 26rem, below the panel's own 448px drag minimum, so no
reachable width could match. It now sits at 30rem, between that minimum and
the default width, and the rail's clip edges and horizontal underline are
percentage-based so a fast resize cannot leave a stale frame behind.

The saved-check flash is gone; a write is acknowledged by the value and the
badge changing, not by an indicator that appears and vanishes. Loading now
renders the widget's skeleton vocabulary, a failed read or write renders an
inline error with a retry, and the optimistic selection rolls back when the
write fails.

SegmentGroup grows a `plain` variant so a consumer can render fully custom
items without the segmented-control chrome, which is what these tiles need;
importing Ark directly is a lint error outside ui-kit-system.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The chip kept its scale and mono typography but nothing said it could be
clicked. It now carries a chevron, a resting border tinted per source, and
the panel's own small-pill interaction recipe lifted from the session
selector trigger: the fill strengthens, the border lifts and the text lifts
over 120ms, and an open menu holds that same look while the chevron flips.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reads now come from the single settings.get call, which carries the
effective value, its controlling layer, each layer's raw value and each
layer's revision. The scope badge derives "use global value" and the
reset layers from that provenance instead of mining the history sidecar,
so the panel makes one read where it used to make two.

Ordinary edits always write the project layer. A value inherited from
the global layer therefore moves to this project when you change it, the
badge flips to PROJECT and the footer says so, and the global value stays
put for other projects. Applying a value to all projects goes through the
single settings.applyGlobally server op rather than a client-sequenced
set plus clear.

Every write sends the layer revision it expects. A revision conflict
reloads the settings and says what happened instead of retrying over a
value someone else wrote; an unparseable settings file says so and offers
no retry until the file is fixed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@omridevk
omridevk force-pushed the issue-572-settings-ui branch from f322635 to 9e4f1e2 Compare August 23, 2026 07:54
Settings routes move under /panel/$sessionId/settings so the chat session
stays mounted while settings is open: back is an instant child swap with the
thread rendered from the live store, live settings repaint keeps working
inside the view, and the back control no longer depends on history state.
The session chrome hides while a settings child is active and the running
blocker exempts settings navigation.

- View motion: settings slides in (anim-tab-right) and plays the presence-out
  exit before navigating back, skipped under reduced motion.
- Horizontal rail: the active-tab underline drew one pixel outside the layer
  svg box (border-box height vs content-box layer) and never showed; plan now
  measures clientHeight. Narrow tab row gets real tab affordances: taller
  rows, top radii, spacing.
- Disabled sections read disabled: mono SOON chip, dimmed icon, not-allowed
  cursor, and the nav is no longer text-selectable.
- The nav rail scales with the panel (clamp over cqi) instead of staying
  156px on a wide panel.
- Scheme writes serialize through a queue and read fresh layer revisions per
  write, so an edit right after applyGlobally no longer races its own
  revision bump into a 409 rollback.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@omridevk

Copy link
Copy Markdown
Contributor Author

New commit: settings nested under the session route (chat stays mounted, back is instant with no refetch flash), enter/exit view motion, horizontal rail underline fixed and tab row redesigned, SOON chips on disabled sections, nav rail scales with panel width, and scheme writes serialize with fresh revisions (fixes the applyGlobally-then-edit 409 race). All verified live in the browser; gates green: 201 app + 22 embed unit + 149 e2e, lint, format, fallow 0 introduced.

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