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
1 change: 0 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ only routes to them.
| Config fields that persist to TOML | `context/config-persistence.md` |
| Database schema migrations or the SQLite connection layer | `context/db-migrations.md` |
| Deferred merge behavior | `context/deferred-merge.md` |
| Embed routes or host bridges | `context/embeds.md` |
| Daemon startup, discovery, host/origin validation, or SSE replay | `context/server-runtime.md` |
| Fleet settings, snapshots, host routing, or peer transports | `context/fleet-architecture.md` |
| API failures or frontend error branching | `context/error-handling.md` |
Expand Down
28 changes: 0 additions & 28 deletions context/embeds.md

This file was deleted.

2 changes: 1 addition & 1 deletion context/mobile-ux.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ Viewport size chooses presentation, not route identity.

Do not collapse these concepts:

- **Compact/narrow presentation**: a desktop window, split pane, or embedded surface that is too narrow for sidebars or dense desktop chrome. It can use focus presentation, but it should retain desktop-scale typography and desktop action geometry.
- **Compact/narrow presentation**: a desktop window or split pane that is too narrow for sidebars or dense desktop chrome. It can use focus presentation, but it should retain desktop-scale typography and desktop action geometry.
- **Phone-like presentation**: a touch/mobile-user-agent context where larger mobile tokens, hit targets, and phone-specific action layouts are appropriate.

A phone stays phone-like in landscape: a coarse-pointer, mobile-user-agent device keeps phone presentation up to the handheld landscape bound, while wider or single-signal devices stay desktop-narrow (`frontend/src/lib/utils/phone-presentation.ts::isPhoneLikeViewport`).
Expand Down
5 changes: 0 additions & 5 deletions context/platform-sync-invariants.md
Original file line number Diff line number Diff line change
Expand Up @@ -513,11 +513,6 @@ Repository import requests and route/query shapes should carry
(`internal/server/pullapi/helpers.go::visibleMergeRequest`,
`internal/server/issueapi/mutation_handlers.go::requireVisibleIssue`,
`internal/db/queries_stacks.go::ListStackPlacementsForMRs`)
- Embedded navigation events for repo-bound routes must publish identity from
parsed route state, not from global embed config. When a route carries repo
identity, event payloads should include `provider`, `platform_host`, and
`repo_path` and may keep `repo` as the display/canonical path. Global
`ui.repo` config is only a fallback for non-repo-bound pages.

## Testing

Expand Down
18 changes: 10 additions & 8 deletions context/ui-design-system.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# UI Design System

Desktop and mobile remain supported application experiences. External host embeds
have no consumers and are retired; keep shared detail and workspace views available
to the application.

Use this document as the intent-level guide for frontend UI work in `kenn-forge`. It should stay short, stable, and useful in model context.

## Purpose
Expand Down Expand Up @@ -90,10 +94,8 @@ otherwise fails only in the Vitest/Playwright transform tier, not in
`frontend/src/lib/components/terminal/agentHarness.ts::harnessForAgentKey`).
The glyph only replaces the generic kind icon; the target's own label always stays.
- Theme resolution: kit's theme store owns dark/light/system resolution
and persistence (`kenn-forge-theme` key); `theme.svelte.ts` adapts it. A
host-forced mode applies classes directly and never persists via
`setThemeMode`; an explicit user toggle persists even under a forced
mode. Relative timestamps use kit `formatRelativeTime`;
and persistence (`kenn-forge-theme` key); `theme.svelte.ts` adapts it.
Relative timestamps use kit `formatRelativeTime`;
`parseAPITimestamp`/`localDate*Label` stay app-side.
- Dialogs: every dialog pushes a keyboard modal-stack frame. Background
Escape surfaces cannot detect dialogs via `defaultPrevented` (kit's
Expand All @@ -113,7 +115,7 @@ otherwise fails only in the Vitest/Playwright transform tier, not in
accent border only; `app.css` drops the kit `TextInput` outline ring
that would otherwise stack on the same wrapper (every tap or Tab into a
text field is `:focus-visible`). Hover-only controls nested inside a
list row button (`.star-btn`, `.import-btn`) carry `tabindex="-1"` so
list row button (`.star-btn`) carry `tabindex="-1"` so
Tab moves row to row instead of stopping on an invisible target.
- Tab strips are one tab stop: only the selected tab has `tabindex="0"`,
Left/Right/Home/End move focus and selection via
Expand Down Expand Up @@ -141,7 +143,7 @@ otherwise fails only in the Vitest/Playwright transform tier, not in
`expandUsed` footprint and blocks re-expansion). Select tabs via
`.kit-top-bar__tabs .kit-top-bar__tab`, never the bare class.
Provider-mode repo selector visibility must not move the tab row; non-provider
modes reserve its footprint unless embed config hides it
modes reserve its footprint
(`frontend/src/lib/components/layout/AppHeader.svelte::reserveProviderRepoSelectorSlot`).
- `AdaptiveActionGrid`: the issue detail action row on every layout
(`frontend/src/lib/components/detail/IssueDetail.svelte::issue-actions-grid`,
Expand Down Expand Up @@ -509,7 +511,7 @@ Promise-required library callbacks may observe `AppExecution.exit`, but the comm

When an `$effect` launches an Effect fiber, wrap `runCommand` itself in `untrack`; fibers begin synchronously, so untracking only program construction can subscribe the outer Svelte effect to the fiber's rune transitions (`frontend/src/App.svelte:542`).

App-wide health polling belongs to the root runtime lifetime, not the full-shell lifetime, because embedded routes still depend on daemon availability (`frontend/src/App.svelte::roborevPollingExecution`).
App-wide health polling belongs to the root runtime lifetime so every route observes daemon availability (`frontend/src/App.svelte::roborevPollingExecution`).

Provider list, activity, and sync controllers expose synchronous launchers; their Effect workflows own cancellation, shared demand, bounded reads, and sequential cadence so Svelte callers never rebuild Promise generations or timer overlap guards (`frontend/src/lib/stores/`).

Expand All @@ -531,7 +533,7 @@ Component lifetime owns polling and live-event subscriptions; teardown interrupt

A `$state` record written by full-object reassignment (`x = { ...x, k: v }`) that is also read inside the same reactive scope — an `$effect`, or a `{@attach ...}` callback, which Svelte runs as one — is a self-referential dependency: Svelte detects it as `effect_update_depth_exceeded` and the attachment tears itself down and reattaches forever. Mutate the specific key instead (`x[k] = v`) (`frontend/src/lib/stores/workspace-host.svelte.ts::registerSlotElement`).

For TypeScript/Svelte state and routing contracts, avoid anonymous object type literals when the shape represents a domain concept that is reused or exposed across modules. Name shared item identity shapes, route payloads, embed callbacks, and API view models near the module that owns the concept, then import those types at call sites. PR/issue/file/focus route identity and URL construction belongs in the shared route item module at `frontend/src/lib/routes.ts`; the frontend router remains the browser-location adapter over those builders. New routed item callers should use those named refs and builders instead of repeating `{ owner; name; number; platformHost }` shapes or hand-building `/pulls`, `/issues`, or `/focus` URLs.
For TypeScript/Svelte state and routing contracts, avoid anonymous object type literals when the shape represents a domain concept that is reused or exposed across modules. Name shared item identity shapes, route payloads and API view models near the module that owns the concept, then import those types at call sites. PR/issue/file/focus route identity and URL construction belongs in the shared route item module at `frontend/src/lib/routes.ts`; the frontend router remains the browser-location adapter over those builders. New routed item callers should use those named refs and builders instead of repeating `{ owner; name; number; platformHost }` shapes or hand-building `/pulls`, `/issues`, or `/focus` URLs.

When TypeScript complains, prefer making the owning type more precise over adding call-site assertions. Generated OpenAPI types, named domain unions, and shared option arrays should carry their real values so components can consume them directly. Good cleanups look like `handleCommandResult(result: void | Promise<void>, ...)` or a typed dropdown option returning `TimeRange`; they remove runtime probing and casts by tightening the contract. Bad cleanups add `as unknown as`, broad `as any`, defensive `instanceof` branches, or response-normalization functions around data that is already typed by the API schema.

Expand Down
25 changes: 9 additions & 16 deletions context/ui-interaction-contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,6 @@ Examples of transient state that should usually reset on identity change:
- inline edit drafts
- merge/close/reopen dialogs
- approve/review forms
- embedded detail-tab selection when the parent surface owns the item

## Persistence Scope

Expand All @@ -216,7 +215,7 @@ Persisted controls must state their scope clearly.
storage (`frontend/src/lib/components/layout/ForgeSelector.svelte`).
- Browser-local preferences belong in `localStorage` only when the behavior is
intentionally per-browser and not worth server settings.
- Agent status visibility uses Forge config across browsers and embedded views.
- Agent status visibility uses Forge config across browsers.
Keep PR, Issue, and Activity labels on the right, matching Workspaces
(`frontend/src/lib/components/shared/AgentStatusIndicator.svelte`).
- `Involves me` is three independent browser-local preferences for Pulls, Issues, and
Expand Down Expand Up @@ -350,10 +349,7 @@ Persisted controls must state their scope clearly.
- Server-backed settings forms must render and build mutations from the reactive
authoritative store; a page-load snapshot can republish stale sibling fields
after hydration (`frontend/src/lib/components/settings/WorkspaceSettings.svelte::workspaces`).
- Settings that select a runtime must hydrate before that runtime starts, but
the gate must abort timed-out or superseded reads and expose retry rather than strand the surface
(`frontend/src/lib/components/terminal/WorkspaceEmbedShell.svelte::loadTerminalSettings`).
- Concurrent startup and embedded-shell callers share the last successful settings snapshot;
- Concurrent startup callers share the last successful settings snapshot;
every accepted settings command invalidates that cache entry through the same acknowledged
workflow, backend readiness is not part of the settings-request timeout, and an invalidated
in-flight read cannot publish into the next generation
Expand Down Expand Up @@ -905,10 +901,6 @@ Rows that contain buttons, links, or toggles need clear event ownership.
`tracked_repo_path`, because selections created from catalog rows use the
current route, which diverges after a provider-side rename
(`frontend/src/lib/utils/repo-filter-values.ts::normalizeInteractiveRepoFilterSelection`).
A host-pinned scope (`ui.hideRepoSelector`) is exempt: with no picker to
rescope, dropping it would unscope every request, so pinned selections pass
through normalization untouched
(`frontend/src/lib/utils/repo-filter-values.ts::normalizeGlobalRepoSelection`).
- Roborev has no event replay cursor: reconnect after authoritative job-list reconciliation; a lost
mutation response retains and fences its original target until authoritative observation, never
replays the write. A confirmed POST stays acknowledged when its follow-up refresh fails; report
Expand Down Expand Up @@ -943,9 +935,8 @@ Rows that contain buttons, links, or toggles need clear event ownership.
- Frontend uncertainty fences live for one browser application runtime. A deliberate reload clears unresolved
evidence, so the user must verify fresh authoritative state before attempting that mutation again
(`frontend/src/lib/app/runtime.ts::makeAppRuntime`).
- Project registration, clone, and new-worktree commands capture host/project identity and remain
application-owned after acceptance; retained worktree acknowledgements are generation-owned, so an
older reconciler cannot clear a replacement command or presentation fence
- Project registration and clone commands capture host identity and remain
application-owned after acceptance; returning to an accepted command must not duplicate it
(`frontend/src/lib/components/terminal/project-mutation-workflow.ts::ProjectMutationWorkflow`).
- Workspace runtime commands remain application-owned after acceptance and retain presentation by
`(hostKey, workspaceId)` across surface replacement; one-shot delete presenters may shadow the route presenter,
Expand All @@ -958,9 +949,6 @@ Rows that contain buttons, links, or toggles need clear event ownership.
browser application runtime; a deliberate page reload clears them, so a user who reloads during an unresolved
outcome must verify authoritative workspace state before attempting the action again
(`frontend/src/lib/components/terminal/workspace-runtime-workflow.ts::makeWorkspaceRuntimeWorkflow`).
- Embedding host callbacks settle only after mutations are durably visible to the next authoritative
snapshot; negative or malformed acknowledgements reconcile before the command is offered again
(`frontend/src/lib/components/terminal/project-mutation-workflow.ts::ProjectMutationWorkflow`).
- Repository-browser commands use a mount-bound facade and fence every state publication;
automatic README-first selection yields to user selection, and stale teardown cannot affect a successor
(`frontend/src/lib/stores/repo-browser.svelte.ts::RepoBrowserMount`).
Expand Down Expand Up @@ -1169,6 +1157,11 @@ responses, and discard stale responses instead of patching another item.
- Onboarding repository setup owns its initial sync through `triggerSyncEffect`: a rejected trigger returns the flow
to a retryable repository step with the failure visible, while an accepted trigger advances only after the ordered
sync command settles (`frontend/src/lib/components/onboarding/OnboardingFlow.svelte::startSync`).
- Project registration and cloning continue to Activity; the workspace list is not
the setup landing page
(`frontend/src/lib/components/terminal/WorkspaceFirstRunPanel.svelte::runProjectSubmission`).
- Removing external embeds does not require a replacement project/worktree screen;
preserve the standalone desktop and mobile workflows.

## Testing Expectations

Expand Down
38 changes: 5 additions & 33 deletions frontend/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,41 +18,13 @@
<script>
// Apply theme before first paint to avoid flash of wrong theme.
(function () {
var cfg = window.__kenn_forge_config;
var mode = cfg && cfg.theme && cfg.theme.mode;
var dark;
if (mode === "dark") {
dark = true;
} else if (mode === "light") {
dark = false;
} else if (mode === "system") {
dark = matchMedia("(prefers-color-scheme: dark)").matches;
} else {
try {
var v = localStorage.getItem("kenn-forge-theme");
} catch (e) {
var v = null;
}
dark = v === "dark" || v === "light" ? v === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
try {
var v = localStorage.getItem("kenn-forge-theme");
} catch (e) {
var v = null;
}
var dark = v === "dark" || v === "light" ? v === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
if (dark) document.documentElement.classList.add("dark");
// Apply color overrides from config before first paint
var colors = cfg && cfg.theme && cfg.theme.colors;
if (colors) {
var map = {
bgPrimary: "--bg-primary",
bgSurface: "--bg-surface",
bgSurfaceHover: "--bg-surface-hover",
bgInset: "--bg-inset",
textPrimary: "--text-primary",
textSecondary: "--text-secondary",
textMuted: "--text-muted",
};
var s = document.documentElement.style;
for (var k in colors) {
if (map[k]) s.setProperty(map[k], colors[k]);
}
}
})();
</script>
</head>
Expand Down
Loading
Loading