feat(frontend): migrate to shared @kenn-io/kit-ui components#957
Conversation
roborev: Combined Review (
|
roborev: Combined Review (
|
roborev: Combined Review (
|
roborev: Combined Review (
|
roborev: Combined Review (
|
roborev: Combined Review (
|
roborev: Combined Review (
|
First stage of the staged migration onto the shared @kenn-io/kit-ui component library (docs/migration.md in the kit-ui repo): consume the library as a source dependency and let its theme.css own the shared design tokens that were originally consolidated *from* this app. app.css keeps only agentsview-specific tokens: the extra agent-identity accents and their -foreground pairs, tool-category colors, duration UX states, message-role backgrounds, and the viewport indicator. Two shared tokens are deliberately overridden: --accent-teal keeps the greener hue (kit-ui's cyan-leaning teal would collide with --accent-cyan, and both are used as distinct agent-identity hues) and --header-height stays at 40px until the AppHeader -> TopBar swap in a later stage. The pinned html font-size is gone in favor of kit-ui's rem type scale (--font-size-root on body), which resizes on handheld touch devices by pointer type instead of viewport width. Visual deltas accepted with the token unification: dark-mode border/green/status-waiting shades shift slightly, and the font stacks lose the Noto Sans/Fira Code fallbacks. kit-ui is a private repo consumed via a file:../../kit-ui path dep, so a sibling checkout is required; CI wiring lands with a later stage. Baseline for the migration burndown: kit-ui-check --warn reports 281 findings in 92 files.
Second stage of the kit-ui migration (kata rv65/2f79): swap the low-risk display primitives for the shared library so agentsview stops carrying private copies of components that kit-ui consolidated from it. StatusDot moves to kit-ui's status-string API: call sites derive the status via getSessionStatus and pass a localized label through the new sessionStatusLabel helper, keeping the i18n contract (shared components take translated strings as props) while the rendering — including the waiting speech bubble and pulse animations — comes from the library. The local component is deleted; TopSessions' test selectors follow the kit- class prefix. Hand-rolled spinners in TrendsPage and ActivityInsight become kit-ui Spinner (wrapped aria-hidden where visible text already announces the state), and empty placeholders in TrashPage, PinnedPage, MessageList, and ActivityInsight become EmptyState — ActivityInsight's deliberately changes from left-aligned to the shared centered layout. SignalPanel's one-line 'not enough activity' note is not an empty state; it keeps its inline strip layout under a non-matching class name. The CommandPalette esc hint becomes KbdBadge; ShortcutsModal's key list keeps raw <kbd> with a reasoned suppression because it enumerates compound alternatives and must stay visible on touch devices, where KbdBadge hides itself. Spinner instances inside AppHeader and RefreshControl are left for the stage-4 rewrites that replace those components wholesale. kit-ui-check --warn: 281 -> 265 findings.
Third stage of the kit-ui migration (kata rv65/ask0): CopyButton, IconButton, and sortable table headers move to the shared library. All five CopyButton call sites keep controlled mode (parent-owned copied state through the app's clipboard util) because tests and copy flows hook that single path; the hover-reveal contract moves to the library's revealOnHover prop with parents revealing .kit-copy-btn. PinnedPage's hand-rolled copy button becomes a CopyButton, which needed a new pinned_copied_message key in all three locales — the shared component takes pre-translated copied labels as props. The local CopyButton component and its test are deleted. The hand-rolled icon buttons in PerfDebugPanel and InsightsPage become IconButton (required ariaLabel, tokenized hover states). SessionsTable's sortable headers become TableHeaderCell — aria-sort, arrow indicator, and numeric right-alignment come from the library; the local table shell keeps its sticky positioning via a :global rule because the header cells now render outside this component's style scope. Deliberately not swapped: CommandPalette's relevance/recency toggle stays hand-rolled because SegmentedControl cannot suppress mousedown focus-stealing, and the palette must keep focus in its input; broad Button adoption is deferred until after stage 4 since most candidate buttons live in modals and headers that stage replaces wholesale. kit-ui-check --warn: 265 -> 255 findings.
Fourth and largest stage of the kit-ui migration (kata rv65/e0hc): the app shell and every overlay now come from the shared library. AppHeader is rebuilt on TopBar: all eight routes are flat primary-nav tabs that collapse into a dropdown by measurement, replacing both the hand-rolled 'More' menu and the 1024/767px width breakpoints; the command-palette hint degrades field-to-icon via FitStages with searchMinWidth sequencing the two breakpoints. The 40px --header-height override is gone — the header standardizes on kit-ui's 44px. Side-region labels drop while the tabs are collapsed (CSS, so jsdom tests still see them). One accepted TopBar limitation: on the settings route the first tab renders as current because TopBar cannot express 'no active tab'. All seven modals move to Modal (focus trap, scroll lock, Escape and overlay-close built in) with kit-ui Buttons in their footers, and the shared .modal-* styles leave app.css; ResyncModal/ImportModal keep their un-dismissable in-progress guards. OptionTypeahead call sites use kit-ui Typeahead directly (identical API — it was extracted from this app) and the local component is deleted; ProjectTypeahead stays as project glue. The usage FilterDropdown becomes a thin wrapper over kit-ui's, keeping the CSV include/exclude semantics app-side. SessionFindBar becomes a store-wiring wrapper over FindBar (pinned variant matches the old presentation). StatusBar keeps its content but renders through kit-ui's left/right regions. RangePicker becomes a ~90-line i18n-injection wrapper (call sites unchanged; the app's resolveRange semantics are preserved — kit-ui's resolver never feeds the backend); RefreshControl maps 1:1 and is deleted in favor of the library component. Known i18n regressions from kit-ui API gaps, to fix upstream: Modal's close X and RefreshControl's freshness label are hardcoded English, and weekOfLabel cannot express zh date-first word order. Kept local with reasoned checker suppressions: the command-palette overlay (top-aligned, palette-owned focus) and ThreeColumnLayout's pointer-capture sidebar resizer. jsdom gets a no-op ResizeObserver stub since TopBar, FitStages, and Typeahead all measure with it. kit-ui-check --warn: 255 -> 216 findings.
frontend/package.json consumes @kenn-io/kit-ui as file:../../kit-ui — a sibling source checkout that exists on dev machines but not in a clean CI, release, desktop, or docker checkout, so every npm ci and frontend build off a fresh clone was broken (roborev 4565/4566/4572/4576, High). Publishing to a registry or vendoring would fork the library away from its consumed-as-source design, so the fix keeps the sibling-checkout contract and reproduces it in CI: a checkout-kit-ui composite action clones kenn-io/kit-ui next to the workspace, pins it to the commit recorded in .github/kit-ui-ref (single place to roll the library forward), and installs its dev dependencies — required because the app build loads kit-ui's svelte.config.js. The action runs before every frontend npm ci: ci.yml (frontend, frontend-node-25, e2e), release.yml (both binary matrices, incl. the manylinux containers — the reason auth is an HTTPS token rather than a deploy key, since those images ship git but not ssh), and the desktop workflows (their sidecar script builds the frontend). Docker builds cannot see siblings of the build context, so docker.yml passes the checkout as a named kit-ui build context and the Dockerfile copies it to /kit-ui, where the lockfile's ../../kit-ui link resolves. kit-ui is private: workflows pass secrets.KIT_UI_TOKEN (fine-grained PAT, contents:read on kenn-io/kit-ui) — an operator must provision that secret once; the action falls back to an unauthenticated clone and fails with instructions otherwise, and keeps working unchanged if the repo ever goes public. Validated by pointing the local dep at a clean clone of the pinned commit (c76a816) and running npm ci, svelte-check, the unit suite, and the production build — all green. The docker path is unverified here (no local daemon); it follows the documented named-context pattern.
…ken ownership Follow-ups from the kit-ui migration reviews: an integration test pins the header copy button's controlled-mode contract (click forwarding into the app clipboard util and the parent-owned copied aria/title state), which would otherwise break silently if kit-ui's CopyButton API or class names change. The app.css comment records that all --accent-*-foreground pairs are agentsview-owned even for kit-ui's core accents, since kit-ui defines no foreground tokens and the inks are tuned to the identity badge fills.
The frontend's @kenn-io/kit-ui dependency moves from file:../../kit-ui to a commit-pinned git dependency (github:kenn-io/kit-ui#<sha>), so the pin lives in package.json/package-lock.json rather than a side-channel .github/kit-ui-ref file, and npm ci resolves it anywhere git credentials for the private repo exist. kit-ui ships only src/lib, bin, and the check rules in its pack (no svelte.config.js), which also removes the need to npm-install kit-ui's devDependencies before building the app; kit-ui-check now runs via npx from node_modules. The checkout-kit-ui composite action (sibling clone + install) is replaced by kit-ui-auth, which only rewrites the kenn-io/kit-ui ssh URL recorded in the lockfile to KIT_UI_TOKEN-authenticated HTTPS. Docker builds drop the named kit-ui build context; npm ci inside the frontend-build stage authenticates the same way via a kit_ui_token BuildKit secret so the token never lands in an image layer. Validation: npm ci from the updated lockfile, svelte-check (0 errors), full unit suite (1724 passing), production build, and kit-ui-check (216, unchanged) all pass locally; actionlint is clean. The Docker image build was not exercised because no local Docker daemon was available.
The kit-ui GitHub dependency was wired up assuming a private repository: a kit-ui-auth composite action rewrote its ssh URL to PAT-authenticated HTTPS in every workflow, and the Docker build threaded a BuildKit secret into npm ci. kenn-io/kit-ui is public, so none of that is needed — npm clones GitHub-hosted git dependencies anonymously over HTTPS even though the lockfile records a git+ssh URL for them. Verified by running npm ci from the unchanged lockfile with ssh disabled (GIT_SSH_COMMAND=/usr/bin/false), an empty git config, and a cold npm cache; the install succeeds. The KIT_UI_TOKEN secret no longer needs to be provisioned. The Docker image build itself was not exercised (no local daemon); actionlint reports only pre-existing shellcheck notes.
…sive semantics Stage 4 left the app with two meanings of "Last N days": kit-ui's RangePicker labels presets and seeds its Custom tab with N calendar days inclusive of today (from = today-(N-1)), while the app's presetRange and rollingRange resolved the same selection to an N+1-day span (from = today-N). Switching from a relative preset to Custom therefore seeded a start date one day later than the range actually being queried. Adopt kit-ui's definition in both helpers so every consumer — preset resolution, rolling window_days URLs, custom-tab seeding, and preset detection — agrees. This is a deliberate behavior change: relative windows now span N days instead of N+1, so "7d" queries one fewer trailing day than before. Existing pinned from/to URLs are unaffected; rolling window_days links rematerialize under the new math. Also from the stage-4 reviews: the dark high-contrast e2e check now measures a real kit-ui solid Button (Import modal confirm) instead of a synthetic element styled with token pairs kit-ui's Button does not actually use, and DESIGN.md documents the range-semantics contract plus the known kit-ui gaps (Modal close-X aria-label, RefreshControl age label, weekOfLabel word order, TopBar no-active-tab) with their resolution path.
Utility implementations duplicated in kit-ui become re-exports so one copy owns the behavior: copyToClipboard, debounce, truncate, formatCost, formatNumber, and formatTokenCount now come from kit-ui's util subpaths, with call sites and tests still importing the app module paths so vitest module mocks keep working. projectColor delegates to kit-ui's hashColor but deliberately keeps the app palette — kit-ui's default palette differs in order and content, and switching would reshuffle every project's established identity color. The i18n-aware formatters (formatRelativeTime, formatTimestamp, formatRefreshAge) stay local on purpose; kit-ui's equivalents are English-only. PublishModal and RemoteSettings drop their hand-rolled navigator.clipboard wrappers for the shared util. Theme state moves to kit-ui's theme store: initTheme reuses the app's historical "theme" storage key (its light/dark values are valid kit-ui modes, so existing preferences carry over) and the legacy agentsview-high-contrast flag migrates to the derived theme-high-contrast key at startup. UIStore.theme/highContrast become getter/setter delegates, kit-ui now owns the root dark/high-contrast classes and persistence, and users without a stored preference gain OS preference tracking via kit-ui's system mode. The desktop postMessage theme:set control keeps working through the setter. Keyboard shortcuts and the markdown pipeline intentionally stay local: the app's shortcut handling is context-dependent imperative code, and the markdown pipeline carries bash wrapper tags, asset URL rewriting, and XML escaping that kit-ui's renderer does not model.
…ange Follow-up to the N-days-inclusive alignment (81ec298): AnalyticsStore and UsageStore still derived their rolling windows with the raw daysAgo(windowDays) N+1 form instead of rollingRange(), so their queries disagreed by one day with the picker, preset detection, and every other rolling consumer. Flagged by roborev jobs 4615/4616 on the alignment commit.
Burns the kit-ui-check backlog down to zero across every component (raw colors mapped to theme/app tokens, off-ladder gaps snapped to the --space-N scale, @media widths snapped to the shared 640/760/900 breakpoints) and turns the checker into a CI gate: the frontend job now runs check:kit-ui without --warn. app.css is exempt from the raw-color rule because it defines the app's token layer, so its color values are definitional; the gate runs the remaining rules over all of src and raw-color over src/lib plus App.svelte. The few deliberate exceptions carry kit-ui-check-ignore comments with reasons: the tuned brown slot of the trends series palette (nearest token would collide with the amber slot), and content/CodeBlock's markup, which stays local because kit-ui's CodeBlock hardcodes self-managed copy and offers no hook for the app's controlled clipboard contract or the in-session find highlighting. The store's isMobileViewport matchMedia moves from 768px to kit-ui's MEDIA.medium (760px) so JS and the newly snapped CSS agree on where the mobile layout starts — previously a 761-767px band reported mobile in JS while CSS rendered desktop.
…rols
The pin moves to kit-ui main at 47ecfd3, which lands the i18n hooks this
migration was waiting on (RefreshControl formatAge, DateRangePicker
weekOfLabel {date} substitution, locale props) plus the RangePicker →
DateRangePicker rename and the popover/tone unification work beneath it.
The shared RangePicker wrapper now passes the week-of template through
with its {date} slot intact, so date-first locales keep their word
order, and a new shared RefreshControl wrapper injects the app's
localized age formatter — closing the two known mixed-language
regressions from stage 4. Both wrappers also pass locale={getLocale()}
(newly re-exported from the i18n facade) so calendar dates and the
refresh tooltip follow the app language setting rather than the browser
locale.
The upgrade also brings a new hand-rolled-popover-card checker rule; the
five header/filter/perf dropdowns adopt the shared kit-popover-card
chrome class instead of restating it in scoped CSS. Tests tracking
kit-ui internals move with upstream: the DateRangePicker mode switch is
now a stock SegmentedControl (radio roles, not tabs) and the trigger
class gained the date- prefix.
…s on content width Two seams left by the breakpoint snap in the stage-6 conformance pass (roborev 4635): SIDEBAR_DESKTOP_BREAKPOINT stayed at 768 while the CSS and ui.isMobileViewport moved to kit-ui's 760px medium breakpoint, so at 761-767px the sidebar rendered desktop CSS with non-desktop resize logic. It now derives from BREAKPOINTS.medium + 1 so every layout decision shares one source of truth. The insights generated-archive grids have hard minimum column widths (controls: 180+130+240px; layout: a 240px list rail), and moving their collapse from 980px to the shared 900px viewport gate reopened a horizontal-overflow window at 901-980px with the sidebar open. They now collapse via a container query on the section's actual inline size, so available content width — not the viewport — decides when they stack. Also bumps the kit-ui pin to 215d252, which hardens the i18n hooks adopted in the previous commit (RefreshControl clamps its clock so custom formatAge formatters never see a negative age; capped Intl caches) with no API changes.
…ules The @container collapse for the generated-archive grids was declared before the base grid-template-columns rules; container queries add no specificity, so source order let the base declarations win and the grids never stacked (roborev 4641). The block now follows both base rules, and the comment records the ordering constraint.
The CI gate previously split the checker into two scoped invocations, path-exempting app.css from the raw-color rule. kit-ui's stage-6 acceptance is the plain full-rule run being green with every exception carrying an in-file kit-ui-check-ignore reason, so the gate is now a single kit-ui-check src over everything and app.css's definitional token lines carry explicit per-line markers instead of a silent path carve-out. One comment mentioning a hex value in prose was reworded rather than suppressed.
…+https The Playwright specs still drove the pre-migration header and status bar: .nav-btn tabs, the removed More-navigation menu (Recent Edits is a top-level TopBar tab now), .status-dot--unclean, and .status-left. They now use the kit-ui selectors. The suite also runs at a 1600px viewport: at Playwright's 1280px default the eight TopBar tabs collapse into the nav SelectDropdown by measurement, so the expanded tab row the navigation specs exercise never rendered. Full chromium suite passes 70/70 (1 skipped). The kit-ui dependency is now declared as an explicit git+https://github.com/kenn-io/kit-ui.git#<sha> spec. npm's lockfile still records the resolved URL in its canonical git+ssh form — npm rewrites it on every install, so that string cannot be pinned to https — but the fetch is anonymous HTTPS regardless (verified with SSH disabled and a cold npm cache); AGENTS.md documents this so it doesn't get re-flagged as an auth problem.
The WebKit CI job failed on two specs. The nav specs assumed the TopBar's expanded tab row, but the collapse decision is by measurement and engine-dependent: WebKit's first layout pass measures the side regions wider than Chromium's, and TopBar freezes that bloated requirement in its expand hysteresis, so at the same 1600px viewport WebKit renders the collapsed SelectDropdown where Chromium renders tabs (upstream kit-ui issue — the frozen expandUsed never re-derives after the transient settles). A shared helper now drives whichever nav mode rendered, and the usage active-tab assertion checks the dropdown value when collapsed. Runtime stability also allowlists WebKit's benign "ResizeObserver loop completed with undelivered notifications" console error, which the TopBar measurement triggers by re-laying-out inside its own observer callback. Both Playwright projects pass locally: chromium and webkit each 70/70 (1 skipped).
…antics TrendsStore initialized its default window with daysAgo(365) + today(), which spans 366 calendar days under the shared N-days-inclusive "last N days" definition. That extra day made TrendsPage's selectionFromRange() treat the default as a custom range instead of the 1y preset. Seed from rollingRange(365) so the default is exactly 365 days inclusive and reads as the relative preset; the window stays rolling (window_days=365 unchanged).
…ahead The origin/main merge added UsagePairwiseComparisonPanel, which imports OptionTypeahead — the local component this branch deleted during the kit-ui migration — so the merged tree failed svelte-check (dangling import) and the kit-ui-check gate (hand-rolled empty-state, off-ladder gap, nonstandard 800px breakpoint). Swap OptionTypeahead for kit-ui Typeahead (drop-in prop-compatible), set the width custom properties on the wrapper so they inherit into .kit-typeahead, rename the inline status notices off the empty-state class (they are one-line panel notes styled with .error-bar, not the centered EmptyState component), and snap the gap and breakpoint to the shared scale — matching how stage 6 handled the rest of usage/.
Bare Trends URLs are supposed to carry rolling one-year intent, but the page initialized its rolling window state as absent and cleared it whenever the URL lacked date params. That made the first materialized URL look fixed, so reloads and shared links could stop advancing after midnight. Keep the shipped default as a 365-day rolling window unless the URL or yoke state explicitly selects a fixed range, and materialize that rolling window before the initial fetch so the first request and URL agree.
61d3570 to
0b3d3c4
Compare
roborev: Combined Review (
|
Migrates the frontend to the shared
@kenn-io/kit-uicomponent library in the six staged commits from kit-ui's migration guide, each independently reviewable and validated: design tokens, display primitives (Spinner, StatusDot, KbdBadge, EmptyState), stateful primitives (CopyButton, Button, TableHeaderCell, Tooltip), overlays and layout (Modal, Typeahead, FilterDropdown, FindBar, StatusBar, TopBar, DateRangePicker, RefreshControl), utilities and the theme store, and finally enforcement.Dependency strategy. kit-ui is consumed as a commit-pinned GitHub git dependency (
github:kenn-io/kit-ui#215d252infrontend/package.json). The repo is public, sonpm ciclones it anonymously over HTTPS everywhere — no sibling checkout, no registry publish, no CI credentials. Bumping the dependency is a one-line hash change plusnpm install. The Docker build needs no special wiring.What stays local, and why. Thin wrappers inject what kit-ui can't know: localized strings and the app locale (
shared/RangePicker.svelte,shared/RefreshControl.svelte), store wiring (SessionFindBar), and domain glue (ProjectTypeahead, the modal family). The i18n-aware formatters, keyboard shortcuts, markdown pipeline, andcontent/CodeBlock(controlled-copy contract) intentionally remain app-owned; DESIGN.md documents each boundary plus the two remaining upstream gaps (Modal close-X aria-label, TopBar no-active-tab state).Deliberate behavior changes.
window_daysURLs, custom-tab seeding, analytics/usage stores) now agrees. Pinned from/to URLs are unaffected."theme"localStorage values carry over, the legacy high-contrast key migrates at startup, and users without a stored preference gain OS-preference tracking.ui.isMobileViewport,SIDEBAR_DESKTOP_BREAKPOINT) derived from the same constants; the insights generated-archive grids collapse on container width because their hard column minimums make viewport gates wrong when the sidebar is open.Enforcement.
npm run check:kit-uigates CI at zero findings (raw colors tokenized, spacing on the--space-Nladder, shared breakpoints,kit-popover-cardchrome). The gate is the plain full-rulekit-ui-check srcrun — no rules disabled, no paths exempted. Every legitimate exception carries an in-filekit-ui-check-ignoremarker with a reason: the definitional token lines inapp.css, the trends brown palette slot, and CodeBlock's copy contract.Where to look. The riskiest changes are the date-window semantics (
utils/dates.ts,shared/dateRangeSelector.ts, and the store updates instores/analytics.svelte.ts/stores/usage.svelte.ts) and the theme-store delegation instores/ui.svelte.ts. The stage-6 commit is broad but style-only. Visual deltas from token snapping were kept conservative but were not screenshot-verified; the deliberate ones are documented in the stage commit messages.🤖 Generated with Claude Code
generated by a clanker