Phase 5 client split: extract @openpalm/client SPA + ui-kit#559
Merged
Conversation
Add ui-client-split-assessment.md: decision record ratifying the host/client split — packages/ui stays the host control plane; chat/connections extract to a thin static packages/client (+ shared packages/ui-kit); PWA ships with two install origins (localhost via harness/CLI, official hosted URL with central TLS). Revise ui-runtime-modes-plan.md accordingly: - Phases 1-4 unchanged; new Phase 1.5 (openpalm admin / host-ui mode) - Phases 5-6 re-scoped to the client extraction; new Phase 6.5 (guardian TLS + CORS workstream, gates the phone story) - [as-landed] corrections: skeleton resolution is a five-strategy chain retaining Electron extraResources; tools use the baked package.json + bun update pattern (supersedes tools.json design); entrypoint install/start_ui already scaffolded, with the OPENCODE_API_URL vs OP_OPENCODE_URL wiring gap recorded - Simplicity guardrails: TLS never a manual user task on default install paths; two UI packages max; pairing is paste-or-scan Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FetR7BttT1xBrA8fBwhui9
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FetR7BttT1xBrA8fBwhui9
TDD red suite for RuntimeContext v2 (plan §6.1-§6.4, §4.3): - lib/server/features.vitest.ts: computeServerRuntimeContext env→hostMode mapping (OP_UI_HOST_MODE > OP_INSIDE_ELECTRON > OP_ENABLE_ADMIN > pwa-static baseline), ServerRuntimeContext contract shape (version 2, security, activeConnectionMode), and per-hostMode serverCapabilities. RED: 15 tests (computeServerRuntimeContext is not exported yet). CHARACTERIZATION (passes pre-change, must stay green): 3 tests pinning computeFeatureFlags().admin under OP_INSIDE_ELECTRON=1 / OP_ENABLE_ADMIN=1. - lib/runtime-context.vitest.ts: resolveCapabilities full matrix per §6.2 (electron=ALL; host:stack:read+browser=all minus electron-only; connections:single→chat+assistant-settings; pwa baseline→connections+ chat+pwa:install; grantedCapabilities union+dedupe; standalone-pwa restriction per §4.2), hasCapability via the runtimeContext store, and §4.3 matrix rows end-to-end. RED: whole file (module does not exist). - routes/api/runtime/server.vitest.ts: GET /api/runtime is public (200 with no/garbage session cookie), returns JSON ServerRuntimeContext with the contract-version handshake field. RED: whole file (+server.ts does not exist). - routes/layout.server.vitest.ts: layout load returns serverRuntimeContext (RED: 3 tests) and keeps the features.admin derived alias (CHARACTERIZATION: 3 tests, green pre-change). Verified red for the right reasons: 21 failed tests / 2 failed suites, all "Cannot find module" / "not a function" / missing-key assertions; server project otherwise at baseline (940 passed incl. the 6 characterization tests). ui:check reports 9 errors, all in these test files, all missing Phase 1 exports — resolves when the implementation lands. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FetR7BttT1xBrA8fBwhui9
…ime (#509) Phase 1 of the ui-runtime-modes plan (docs/technical/ui-runtime-modes-plan.md §6.1-§6.4): replace the single features.admin boolean with a capability-based ServerRuntimeContext while keeping zero behavior change for existing routes. - lib/types.ts: UiHostMode, Capability, ServerRuntimeContext (contract version 2), ClientDisplayMode, ConnectionKind, ActiveConnectionContext, ClientContext, RuntimeContext per plan §6.1. FeatureFlags kept as the documented derived alias. - lib/server/features.ts: computeServerRuntimeContext(event). hostMode resolution: OP_UI_HOST_MODE (explicit) > OP_INSIDE_ELECTRON=1 → electron-host > OP_ENABLE_ADMIN=1 → host-ui > pwa-static baseline. Per-mode serverCapabilities (host modes carry host:*, never connections:single; assistant-container is connections:single + chat + assistant-settings:*; pwa-static is connections:* + chat + pwa:install). computeFeatureFlags() is now derived from hostMode — identical results for every env combination in use today. - lib/runtime-context.svelte.ts: resolveCapabilities(serverCaps, clientCtx) verbatim per plan §6.2 — the ONLY place capability logic lives — plus the reactive runtimeContext store, hasCapability(cap) (UX only; APIs enforce capabilities server-side), and initializeRuntimeContext(). - lib/client-context.ts: detectClientDisplayMode() per plan §6.3, browser-side only. - routes/+layout.server.ts: returns serverRuntimeContext alongside the features alias. routes/+layout.svelte: initializes the client context in onMount (client-only — never server-computed, and never mutating the module-level store during SSR) and derives effectiveCapabilities. - routes/api/runtime/+server.ts: public GET (no auth) returning the ServerRuntimeContext — the contract-version handshake for future hosted clients. Turns the 21-test red suite from 483b4bf green without modifying it; the 6 characterization tests (features.admin alias) stay green. Verified against a live dev server: /api/runtime returns 200 with no/garbage session cookie in pwa-static and host-ui modes; / and /login routing unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FetR7BttT1xBrA8fBwhui9
TDD red suite for `openpalm admin` (host-ui mode; plan Phase 1.5, §4.1,
§8.3) in packages/cli/src/commands/admin.test.ts:
- Registration + help. RED: 2 tests — 'admin' missing from the main.ts
subCommands map; `renderUsage(mainCommand)` COMMANDS table has no admin
row.
- Serve mode, driven through the command via citty runCommand against a
seeded temp OP_HOME (no module mocks: real lib + cli-state; fetch
answers /health ready and fails registry pulls non-fatally; Bun.spawn
captured so nothing real launches). RED: 3 tests, all failing with
"Cannot find module .../src/commands/admin.ts":
* spawned UI child env carries OP_ENABLE_ADMIN=1 + OP_UI_HOST_MODE=
host-ui, binds 127.0.0.1 with pinned loopback ORIGIN, prints the URL,
opens the browser by default;
* loopback enforcement — OP_ALLOW_REMOTE_SETUP=1 is ignored (loopback
HOST/ORIGIN, no HOST_HEADER/PROTOCOL_HEADER) and neutralized in the
child env (isRemoteSetupAllowed(childEnv) === false) so the respawned
`openpalm ui` child and the UI's remote-setup relaxations cannot
re-derive a remote bind; --no-open spawns no opener;
* with an empty OP_HOME (not installed) the command still serves the
admin-enabled UI — the UI's existing guard lands on /setup; the CLI
must not demand `openpalm install` or reimplement wizard logic.
- CHARACTERIZATION (green pre-change, must stay green): 2 tests pinning
the bare `openpalm` serve path's spawn env — loopback + pinned ORIGIN
with NO admin/mode env introduced, and OP_ALLOW_REMOTE_SETUP still
producing the 0.0.0.0 + HOST_HEADER remote bind. Loopback-always is
scoped to the admin command only.
Verified red for the right reasons: 5 fail / 2 pass in the new file;
full CLI suite 82 pass / 6 fail = these 5 + the 1 known uid-0
install-flow env failure. biome lint: no findings in the new file;
packages/cli tsc --noEmit exit 0 (red-state safe via computed-specifier
dynamic import); ui:check 0 errors 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FetR7BttT1xBrA8fBwhui9
Register a new `admin` subcommand that serves the existing UI through the existing startUIServer supervisor with the admin capability enabled in the spawned UI child (OP_ENABLE_ADMIN=1 + OP_UI_HOST_MODE=host-ui), prints the URL, and opens the browser (reusing the existing openBrowser helper; --no-open suppresses it). Loopback-only ALWAYS (plan §8.3): admin mode ignores OP_ALLOW_REMOTE_SETUP when deriving the bind (HOST=127.0.0.1 with a pinned loopback ORIGIN, no HOST_HEADER/PROTOCOL_HEADER) and neutralizes the flag in the child env (OP_ALLOW_REMOTE_SETUP=0) so neither the respawned `openpalm ui` child nor the UI server's own remote-setup relaxations can re-derive a remote bind. The bare serve path keeps its current behavior (characterization tests pin it): loopback by default, 0.0.0.0 + Host-header origin under OP_ALLOW_REMOTE_SETUP. On a machine with no install the command still serves — resolveServeState() tolerates a not-installed OP_HOME so the UI's existing setup guard takes over; the CLI reimplements no wizard logic. No new auth mechanism: the UI's existing op_session password auth applies unchanged. Makes the Phase 1.5 red suite (admin.test.ts) green without modifying it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FetR7BttT1xBrA8fBwhui9
Update the how-it-works harness UI section: Electron (electron-host mode), `openpalm admin` (host-ui mode, loopback-only, refuses non-loopback bind config), and the dev-only OP_ENABLE_ADMIN=1 escape hatch. Drops the stale `openpalm ui serve` reference. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FetR7BttT1xBrA8fBwhui9
TDD red suite for connection management out of /admin (plan ui-runtime-modes-plan.md Phase 2, issue #486): - routes/api/connections/server.vitest.ts: /api/connections is guarded server-side by the connections:manage capability (plan §6.4, §8.5) — 403 JSON in assistant-container mode even with a VALID admin session (capability-based, not session-based), 200 in host-ui mode, reachable in pwa-static mode (no host-admin mode required), lists the env-derived default connection, never serializes stored passwords; POST guarded the same way (403 assistant-container / 201 host-ui). RED: all 8 tests — "Cannot find module .../+server.js" (route module does not exist yet; loaded via a computed-specifier dynamic import so svelte-check stays green while red). - lib/server/endpoints-connection-kind.vitest.ts: ConnectionKind defaulting when reading a legacy endpoints.json fixture (plan §6.6) — records without a kind key default to 'remote-opencode' on read (listEndpoints + getActiveEndpoint); the env-derived default entry is 'local-opencode'. RED: 4 tests ("expected undefined to be 'remote-opencode'/'local-opencode'" — kind read via a cast helper so svelte-check stays green). CHARACTERIZATION (green pre-change, must stay green): 3 tests — an explicitly persisted kind passes through on read (unknown on-disk keys already survive; Phase 2 must not strip or override it), writes keep the on-disk schema keys activeId/endpoints (never 'connections'), and legacy records load with no data migration. - routes/admin/endpoints/page.vitest.ts: /admin/endpoints becomes a redirect alias to /connections for 0.13.0 (Phase 2 step 5), pinned as a universal load in +page.ts throwing redirect() with location '/connections' and a 3xx status — same convention as the existing routes/+page.ts → /splash redirect. RED: both tests ("Cannot find module .../+page.js"; computed-specifier dynamic import keeps svelte-check green). - lib/endpoints-state-hygiene.vitest.ts: source-level hygiene test for the untangle step (Phase 2 step 6) — the connections store (follows the file across the endpoint→connection rename: endpoints-state or connections-state) must have NO static, type, side-effect, or dynamic import matching chat-state or chat/*. RED: 1 test (found './chat/chat-state.svelte.js'). CHARACTERIZATION (green): 1 test — the store module exists in $lib. Verified red for the right reasons: server project 15 failed / 992 passed across 106 files; the only failing files are these four, all failures are module-resolution or missing-feature assertions (no syntax errors). ui:check: 0 errors 0 warnings (red-state safe via computed-specifier dynamic imports and cast helpers). biome lint: baseline unchanged (16 warnings 1 info, none in the new files); eslint clean on the new files. CLI suite 87/1 (the 1 = known uid-0 install-flow env failure); lib suite 845 pass / 10 skip / 17 fail (the known uid-0 set) — parity. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FetR7BttT1xBrA8fBwhui9
…ints store (#486) Phase 2 step 1-2 of docs/technical/ui-runtime-modes-plan.md: the internal model is renamed endpoint → connection (plan §6.6) and ConnectionEntry gains an optional `kind` field. Kind is defaulted at READ time only — legacy user records (no kind on disk) read as 'remote-opencode', the env-derived default entry and the Electron-local entry as 'local-opencode', and an explicitly persisted kind passes through untouched. endpoints.json keeps its name and on-disk schema keys (activeId/endpoints); no data migration, files are never rewritten on read. The endpoint-language exports remain as aliases of the renamed functions/types for the pre-Phase-2 /admin routes and tests; they go away with /admin/* in Phase 4. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FetR7BttT1xBrA8fBwhui9
Phase 2 step 3 (plan §6.4 API namespace table): /api/connections, /api/connections/[id] and /api/connections/active delegate to the same lib/server/endpoints.ts module as the legacy /admin/endpoints routes but are guarded by a new requireCapability() helper instead of the host-admin namespace. The guard is enforced SERVER-SIDE (plan §8.5) and is capability-based, not session-based: assistant-container mode gets a 403 JSON envelope even with a valid admin session, while host-ui, electron-host and pwa-static (which advertise connections:manage) pass through to the existing admin-session auth. requireCapability() only reads the capability set advertised by computeServerRuntimeContext — capability logic itself stays in one place (plan §8.6). Stored connection passwords are never serialized (hasPassword only), matching the /admin/endpoints publish() contract. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FetR7BttT1xBrA8fBwhui9
…-events (#486) Phase 2 step 6 (plan §6.11): break the endpoints-state ↔ chat-state bidirectional import — a hard prerequisite for the Phase 5 client extraction. The connections store no longer imports chat modules; it emits through the new $lib/connection-events channel and the chat store subscribes from its own side: - registerActivationGuard: chat vetoes mid-generation switches with the same user-facing message as before ("Wait for the current reply…"). - onConnectionActivated: chat.onEndpointChanged() runs on activation, awaited by the emitter so a failed handoff still rolls the activeId back (behavior preserved from the direct call). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FetR7BttT1xBrA8fBwhui9
…dpoints (#486) Phase 2 steps 3-5 (plan ui-runtime-modes-plan.md): - New /connections page — the existing endpoint management UI restyled minimally in connection language, backed by the capability-guarded /api/connections/* routes so it is reachable in every mode that advertises connections:manage (host-ui, electron-host, pwa-static), without the host-admin /admin namespace. - lib/api/endpoints.ts rewritten in connection language and pointed at /api/connections/*; the connections store consumes it. AssistantEndpoint stays as a type alias until Phase 5 relocates the module. - /admin/endpoints is now a 0.13.0 redirect alias: a universal load in +page.ts throws redirect(302, '/connections') (same convention as the routes/+page.ts → /splash redirect); the sibling JSON +server routes stay for out-of-process callers this release. The hooks /admin gate honors the alias in non-admin modes too, so stale links land on /connections instead of /chat. - Chat garden + settings drawer links now point at /connections. (EndpointList's link is left on /admin/endpoints deliberately — its browser test pins the href and cannot run in this environment; the alias redirect keeps it working. Migrate with Phase 3 chrome work.) - hooks.server.ts exempts /connections from launch routing; the runtime context route pointers advertise connections: '/connections' for host-capable and pwa-static modes. Host mode behavior is unchanged: host admin stays loopback-only and the admin session still gates the connection API after the capability check. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FetR7BttT1xBrA8fBwhui9
…486) The Phase 2 step-3 link migration (/admin/endpoints -> /connections) covered chat/+page.svelte and SettingsDrawer.svelte but missed the identical link in the chat EndpointSwitcher popover, leaving it dependent on the 0.13.0 alias redirect that Phase 4 removes. Update the href and the colocated browser test's assertion to match. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FetR7BttT1xBrA8fBwhui9
TDD red suite for capability-driven landing + navigation + chrome untangle (plan ui-runtime-modes-plan.md Phase 3, §6.5; issue #555): - lib/resolve-landing.vitest.ts: resolveLanding(ctx, launchState) full §6.5 matrix — host:setup rows (migration pending → /attention with precedence over every local state; not_installed/setup_incomplete → /setup; installed_offline → /admin; installed_broken → /admin?tab=diagnostics; running → /chat), assistant-container → /chat always (local state, connections, and migration ignored), pwa-static → /connections/new when 0 connections else /chat, and the gate being CAPABILITY-driven (host-ui ctx without host:setup in effectiveCapabilities falls through to /chat). Pins that resolveLanding is PURE (reads ctx.effectiveCapabilities, never the global store) so hooks.server can call it per-request. Phase 3 keeps the host landing at /admin — HOST_ADMIN_LANDING in the test mirrors the implementation's TODO constant; Phase 4 flips both to /host. RED: all 17 tests ("resolveLanding module not found" — module does not exist; computed-specifier dynamic import keeps svelte-check green while red). - hooks.server.landing.vitest.ts: root + /splash document navigations route through the resolved landing — / with not_installed → /setup and installed_offline → /admin (both currently /splash); /splash is no longer exempt and redirects pre-auth to the resolved landing (/chat when running, /admin when offline, /setup when not_installed); the /splash route files are deleted; /attention exists. RED: 7 tests (redirect location mismatches — currently /splash or /login?redirectTo=%2Fsplash — plus the two route-split file assertions). CHARACTERIZATION (green pre-change, must stay green): 1 test — / with a healthy running stack redirects to /chat. - lib/features-admin-hygiene.vitest.ts: no .svelte component reads features.admin / featuresService.admin (plan §8.6; the alias may survive only in hooks.server pending Phase 4). RED: 1 test (offender lib/components/chrome/Navbar.svelte:70). CHARACTERIZATION (green): 1 walker-sanity test. - lib/components/chrome/chrome-untangle-hygiene.vitest.ts: the chrome module(s) the admin surface mounts (resolved from routes/{admin,host}/+{page,layout}.svelte imports, so the test follows the Phase 3 chrome split and the Phase 4 rename) import no chat components or chat stores. RED: 1 test (4 offenders: Navbar.svelte → components/chat/{EndpointSwitcher,SessionPicker, VoiceControl}.svelte + $lib/chat/navigation.js). CHARACTERIZATION (green): admin surface mounts a chrome module. - routes/chat/page-imports.vitest.ts: chat/+page.svelte does not import the $lib/api.js barrel (direct domain-client imports like $lib/api/chat.js stay allowed). RED: 1 test (offender '$lib/api.js'). CHARACTERIZATION (green): the chat page exists. Verified red for the right reasons: server project 27 failed / 1011 passed across 111 files; the only failing files are these five and every failure is a missing-module or missing-feature assertion (no syntax errors). ui:check: 0 errors 0 warnings (red-state safe). biome: clean on the new files (baseline elsewhere unchanged); eslint: clean on the new files. CLI suite 87 pass / 1 known uid-0 env failure; lib suite 845 pass / 10 skip / 17 known uid-0 failures — parity. No production code changed. Playwright e2e specs are deliberately not added here: browsers cannot run in this container and the Phase 3 route split they would smoke-test lands with the implementation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FetR7BttT1xBrA8fBwhui9
…sh into /attention (#555) Plan ui-runtime-modes-plan.md Phase 3 steps 1-2 (§6.5): - New $lib/resolve-landing.ts: pure resolveLanding(ctx, launchState) — reads ctx.effectiveCapabilities (never the global store) so hooks.server can call it per-request. LaunchState derives from the launch-status probes that fed /splash (LocalStackState + connections + a migration gate). The host landing stays '/admin' behind the HOST_ADMIN_LANDING TODO constant; Phase 4 flips that one value to '/host'. One documented delta from the §6.5 pseudocode: the host not_installed row lands on /chat when an accessible connection exists, preserving the authoritative #440 remote-only rule (nothing local to set up is no reason to block a working remote) — mirrors the pwa-static row's shape. - New $lib/server/landing.ts: resolveRequestLanding(event) collects the launch facts (moved from hooks.server.ts, same 5s cache; _resetLaunchCache is re-exported from hooks for the existing test import site), resolves capabilities with the 'browser' baseline (display mode is client-only by design, §6.3 — resolveCapabilities() remains the only capability logic, §8.6), and runs resolveLanding(). The pre-Phase-3 nuance is preserved: an interrupted install with running containers lands on /setup, not chat. - hooks.server.ts: the launch-routing guard now redirects document navigations through the resolved landing instead of the binary recommendedRoute → '/chat'|'/splash' table. /splash is no longer exempt: the ROUTE is removed and the path 302s pre-auth to the resolved landing for this release only. - Route split: routes/splash/* deleted; routes/attention/+page.svelte is the migration/blocking surface (nothing produces migration 'pending' yet — the gate is wired ahead of the first blocking migration); /connections/new aliases to /connections?new=1 for the pwa-static zero-connections landing; routes/+page.ts's static /splash redirect becomes +page.server.ts resolving the same landing for client-side navigations. - hooks.server.vitest.ts: 'first-run document navigation routes to splash instead of setup' pinned the pre-Phase-3 landing; the ratified Phase 3 matrix (red-suite hooks.server.landing.vitest.ts pins the same scenario) sends not_installed to /setup, so the expectation is updated — the companion rule (not_installed + accessible remote → /chat) still passes unmodified. e2e/setup-guard.pw.ts's loose landing regex swaps splash for the new landings (Playwright cannot run in this container; not executed). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FetR7BttT1xBrA8fBwhui9
…m chat (#555) Plan ui-runtime-modes-plan.md Phase 3 steps 3-4: - chrome/Navbar.svelte becomes the chat-free shell: brand + the chat/host utility button + theme toggle, with a children snippet for surface controls. Destinations come from runtimeContext.routes and visibility from hasCapability('host:stack:read') — the legacy admin feature flag is gone from every component (hygiene test lib/features-admin-hygiene.vitest.ts); the alias survives only in server code (hooks.server.ts / +layout.server.ts, whose layout-data contract stays pinned) pending Phase 4. lib/features.svelte.ts (client featuresService) is deleted. - New chrome/ChatNavbar.svelte: the conversation-surface composition — shell plus EndpointSwitcher, SessionPicker, VoiceControl, ModeSwitch and the session-aware brand href. /advanced and /connections mount it; /admin mounts the shell directly, so the admin surface no longer drags chat components/stores into the host bundle (chrome-untangle-hygiene.vitest.ts, the #555 prerequisite for the Phase 5 client extraction). - chat/+page.svelte imports probeChatBackend from the $lib/api/chat.js domain client instead of the $lib/api.js barrel, which re-exports every admin domain client (routes/chat/page-imports.vitest.ts). - routes/admin/+page.svelte honors the ?tab=diagnostics landing deep-link (installed_broken → Systems tab); routes/connections/+page.svelte opens the add form when landed via /connections/new (?new=1). Behavior deltas that follow from the untangle: the admin surface no longer renders voice controls or the advanced-mode switch (they are chat chrome), and its back-to-chat button targets the plain chat route rather than a session-restoring path (session paths require chat-store imports). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FetR7BttT1xBrA8fBwhui9
) Carried from Phase 0's outstanding items into Phase 3, where the route structure it documents actually changed (plan ui-runtime-modes-plan.md Phase 3 step 5): the §6.5 landing table as implemented, the guard inventory (auth, admin gate, requireAdmin, requireCapability, setup localhost, host/origin), every page route with surface + guard, the API namespaces, and the #555 chrome composition map. Notes that Playwright suites need a real browser + Docker and cannot run in the sandboxed CI container. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FetR7BttT1xBrA8fBwhui9
TDD red suite for the Host/Assistant control-plane split (plan ui-runtime-modes-plan.md Phase 4, §6.4, §8.5; issue #555): - hooks.server.admin-404.vitest.ts: /admin/* is a dead namespace — no alias, no gate redirect; authenticated /admin doc nav falls through to the router 404 (resolve stub emulates the router from route files on disk); /admin/endpoints stops aliasing to /connections; non-host modes stop redirecting /admin/* to /chat; routes/admin/ deleted and routes/host/+page.svelte exists; HOST_ADMIN_LANDING flips to '/host', installed_offline lands on /host, runtime context routes.host is '/host'; hooks.server.ts drops the computeFeatureFlags/features.admin route gate (Phase 4 step 4 — whether the derived alias in features.ts / +layout.server.ts can also be deleted is left to the implementation's grep; today +layout.server.ts still returns it). RED: all 10 tests (200-instead-of-404, 302 redirects to /connections and /chat, '/admin' landing/pointer values, gate still present). - routes/api/host/health/server.vitest.ts: representative privileged /api/host/* endpoint (mechanical move of /admin/health). 403 in assistant-container EVEN WITH a valid admin session (capability-based, not session-based; body.error === 'capability_not_available'); 403 in pwa-static; 200 in host-ui and electron-host; 401 without a session (requireAdmin stays in addition to the capability guard). RED: all 6 ("Cannot find module './+server.js'" — computed-specifier dynamic import keeps svelte-check green while red). - routes/api/host/stack/server.vitest.ts: host-scoped half of the old /admin/assistant endpoint (AssistantTab split). GET/PUT projectName + lanExposureEnabled at /api/host/stack; payload carries NO personaContent; PUT no longer requires it; assistant-container gets 403 with a valid session and stack.env stays untouched (acceptance: cannot edit project name or bind address); 401 unauthenticated. RED: all 7 (module missing). - routes/api/assistant/persona/server.vitest.ts: assistant-owned half — GET/PUT /api/assistant/persona over config/assistant/persona.md; assistant-container CAN read/write (acceptance); host-ui keeps assistant-settings:write; pwa-static 403 with a valid session (capability_not_available); 401 unauthenticated. RED: all 7 (module missing). - routes/api/assistant/akm/server.vitest.ts: assistant-scoped AKM config (AkmTab split) — GET/PATCH /api/assistant/akm over config/akm/config.json; assistant-container CAN edit (acceptance); pwa-static 403; 401 unauthenticated. RED: all 5 (module missing). - routes/api/host/guard-hygiene.vitest.ts: every routes/api/host/**/+server.ts contains a requireCapability( call (plan §8.5 — server-side enforcement; the walker requires >= 20 moved endpoints so the per-file check can never pass vacuously); host-level AKM sharing lives at /api/host/akm/host-sharing and not under /api/assistant. RED: 3 tests (routes/api/host does not exist). CHARACTERIZATION (green today, must stay green): 2 constraint tests — session-lifecycle endpoints are NOT under /api/host (a capability guard on login would lock assistant-container out before it could authenticate) and connections stay at /api/connections (the Phase 2 EXCEPT clause). - lib/api/admin-paths-hygiene.vitest.ts: no quoted '/admin' path survives in lib/api domain clients or in .svelte components outside the deleted routes/admin tree (dead calls/links once /admin 404s), and the login page posts to an existing auth endpoint outside /admin (acceptance: login/session flow unchanged — only the address moves; the exact new path, e.g. /api/auth/login, is the implementation's choice, pinned only as "exists and not /admin"). RED: 3 tests (offenders: 9 lib/api clients — addons, akm, automations, backups, containers, providers, secrets, versions, voice; 7 .svelte files — EndpointList, ModeSwitch, SettingsDrawer, AkmHealthReportSection, attention/+page, chat/+page, login/+page). CHARACTERIZATION (green): 1 walker-sanity test. NOTE for the implementer: resolve-landing.vitest.ts, hooks.server.landing.vitest.ts and ui-route-map.md still pin the Phase 3 '/admin' values via their documented TODO(phase-4) constants — flip them with the implementation; this suite pins the '/host' side. Verified red for the right reasons: server project 41 failed / 1041 passed across 118 files; the only failing files are these seven and every failure is a missing-module or missing-feature assertion (no syntax errors). ui:check: 0 errors 0 warnings (red-state safe). biome: baseline unchanged, no findings in the new files; eslint: clean on the new files. CLI suite 87 pass / 1 known uid-0 env failure; lib suite 845 pass / 10 skip / 17 known uid-0 failures — parity. No production code changed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FetR7BttT1xBrA8fBwhui9
Session auth (login/logout/session) leaves the dying /admin namespace but does NOT move under /api/host — a capability guard on login would lock assistant-container out before it could ever authenticate for /api/assistant/* (pinned by routes/api/host/guard-hygiene.vitest.ts). The flow itself is unchanged: only the address moves. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FetR7BttT1xBrA8fBwhui9
… + /api/assistant (#555) Plan ui-runtime-modes-plan.md Phase 4 (§6.4, §8.5). /admin/* is now a dead namespace: the route tree is deleted, hooks neither gate nor alias it, and requests fall through to the router 404 (no alias — ratified §2). - Pages: routes/admin/+page.svelte → routes/host/+page.svelte. HOST_ADMIN_LANDING flips to '/host' (installed_offline/broken land there); runtime context routes.host is '/host'; nav/components (EndpointList, SettingsDrawer, ModeSwitch, attention, chat) point at /host. - APIs: the ~50 privileged /admin/* JSON endpoints move to /api/host/* and EVERY one now carries a server-side requireCapability('host:…') guard in addition to requireAdmin — a valid admin session in a mode without host capabilities (assistant-container, pwa-static) gets 403 capability_not_available (§8.5: capabilities are enforced server-side; hasCapability() is UX only). Hygiene test guard-hygiene.vitest.ts enumerates the tree and asserts the call in each file. - New /api/assistant/* namespace (assistant-settings:read/write guards): persona (from the old /admin/assistant), akm config (from /admin/akm), model (from /admin/opencode/model — OpenCode's own setting, plan step 2 'persona, model'). assistant-container CAN edit these; pwa-static cannot. - AssistantTab split: host stack settings (projectName, lanExposureEnabled → GET/PUT /api/host/stack, host:stack:write) vs assistant persona (GET/PUT /api/assistant/persona) with separate save actions and hasCapability-driven visibility. AkmTab split: config megaform is assistant-scoped; index maintenance/diagnostics (reindex, stats, health-report) are host-gated; host key sharing stays host-only at /api/host/akm/host-sharing. - hooks.server.ts: the features.admin route gate is replaced by a capability check on /host/*; the Phase 2 /admin/endpoints alias is removed; the X-Frame-Options exception follows the health-report move. - features.admin derived alias DELETED (plan step 4): grep found no readers left — hooks and +layout.server.ts were the last. computeFeatureFlags/ FeatureFlags are gone; the Phase 1 characterization tests that protected the alias through Phases 1–3 (features.vitest.ts, layout.server.vitest.ts) are retired with it, as those tests' own Phase-4 escape hatch and the plan authorize; the env → hostMode mapping they pinned stays covered by the serverRuntimeContext tests. - lib/api domain clients target /api/host/* + /api/assistant/*; the old /admin/assistant client splits into fetchHostStackSettings/ saveHostStackSettings + fetchAssistantPersona/saveAssistantPersona. - Moved colocated route tests run as OP_UI_HOST_MODE=host-ui (the endpoints are capability-guarded now); the old /admin/assistant suite's rename/LAN behavior survives as api/host/stack/rename.vitest.ts minus persona. computeServerRuntimeContext tolerates url-less test event stubs (publicBaseUrl only). Phase 3 TODO(phase-4) constants in resolve-landing.vitest.ts / hooks.server.landing.vitest.ts flip to '/host' as their comments instructed. e2e suites follow the new paths. - Phase 2 EXCEPT clause honored: connections stay at /api/connections; session lifecycle stays outside the capability namespaces at /api/auth. Server suite: 1073/1073 pass. ui:check 0 errors 0 warnings. eslint clean on changed files; biome baseline unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FetR7BttT1xBrA8fBwhui9
…#555) ui-route-map.md reflects the post-Phase-4 truth (/host page, /api/host/* with per-endpoint requireCapability guards, /api/assistant/*, /api/auth/*, /admin/* → 404). api-spec.md and managing-openpalm.md endpoint paths follow the namespace move, with a Conventions note on the capability guards. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FetR7BttT1xBrA8fBwhui9
…555) Review-gate fixes for Phase 4: two browser-project (*.svelte.vitest.ts) tests still referenced the dead /admin namespace and would fail on CI where Playwright browsers are installed: - EndpointList.svelte.vitest.ts: the 'Manage this assistant…' link now resolves to /host, not /admin — assert the new href. - ProvidersPanel.svelte.vitest.ts: fetchSecretFile() now requests /api/host/secrets/op_api_key — match the mock (and its comment) to the new path so the reveal-API-key test exercises the secret branch again. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FetR7BttT1xBrA8fBwhui9
#486, #555) Flip the plan status line, the section 7 phase headers, and the section 10 milestone summary to DONE with one-line as-built notes where the landed code deviated from the design: RuntimeContext v2 lives in lib/server/features.ts (filename kept), the Phase 2 /admin/endpoints alias was removed again in Phase 4, /splash became /attention, no new Playwright smoke tests (existing e2e stack suites updated instead), session auth moved to /api/auth/*, and the assistant namespace split into persona/model/akm. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FetR7BttT1xBrA8fBwhui9
Test-first coverage for the P5a ui-kit extraction (plan
ui-runtime-modes-plan.md §6.11, Phase 5 step 1):
- packages/ui/src/lib/ui-kit-extraction.vitest.ts — RED (4 tests, fail
for the right reason): components/{common,icons} must leave
packages/ui/src/lib, no ui source may keep importing the old
$lib/components/{common,icons} paths (233 offenders today), and ui
must consume @openpalm/ui-kit.
- packages/ui-kit/tests/no-app-coupling.test.ts — HYGIENE,
already GREEN (labeled as such): no ui-kit source file may import
$lib/api, $lib/server, @openpalm/lib, chat state, or the
endpoints/connections store; includes a post-move guard against a
forever-vacuous scan.
- packages/ui/src/lib/ui-kit-components-characterization.vitest.ts —
CHARACTERIZATION, already GREEN (labeled as such): SSR-renders
IconButton + IconClose resolved from ui-kit first with fallback to
the current location, pinning "zero behavior change" across the move
(verified green against a simulated post-move layout too).
Scaffolding only (inert, no production code): packages/ui-kit
package.json (private, bun test script, no exports) + root workspace
entry + lockfile.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FetR7BttT1xBrA8fBwhui9
… @openpalm/ui-kit (#555) P5a of the client split (plan ui-runtime-modes-plan.md §6.11, Phase 5 step 1): packages/ui-kit is now a real raw-source workspace package — private, no build step; its exports point at .svelte/.ts/.css source and the consuming app's Vite/Svelte 5 pipeline compiles it as part of the app build. Moved out of packages/ui/src/lib: - components/common/ (all 21 components + their co-located *.svelte.vitest.ts browser tests and harnesses) - components/icons/ (all 64 icons + index barrel) - the Stillness custom-property token blocks and the wiz-* design vocabulary from app.css -> ui-kit/src/lib/theme/tokens.css, @imported by ui's app.css (ui-specific layout CSS stays in the app; the @import sits above all rules because CSS ignores late imports) Wiring: - ui devDependency @openpalm/ui-kit workspace:*; every ui import rewritten $lib/components/{common,icons}/... -> @openpalm/ui-kit/components/{common,icons}/... (63 files) - kit-internal imports rewritten to relative paths (the old $lib paths no longer exist) - vite.config: ssr.noExternal += @openpalm/ui-kit in dev (raw .svelte source cannot be require()d by node), optimizeDeps.exclude, and the vitest browser project also collects ../ui-kit/src/**/*.svelte.vitest so the moved tests keep running through ui's browser setup (ui-kit deliberately has no vitest of its own; bun's isolated linker means ui-kit declares svelte/vitest/vitest-browser-svelte devDeps so the test files' bare imports resolve) Presentational-only rule (§6.11/§8): SecretSelect was the one component importing $lib/api; its fetchSecretFiles/saveSecretFile are now injected as required props and the single call site (AddonsTab) passes the api functions — zero behavior change. Toast/ThemeToggle/Drawer/FriendlyError keep their $lib service imports (notifications, theme-state, focus-trap, error-messages type), which the hygiene test deliberately permits; they resolve through the consuming app's alias. Verified: ui:check 0/0; ui vitest node project 1082 passed 0 failed (4 P5a red tests now green) with all 30 browser files still collected (known chromium-download env failure unchanged); bun test ui-kit 2/2; cli/lib/electron suites at baseline parity; production build inlines tokens.css; dev-server curl checks green in host-ui and pwa-static modes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FetR7BttT1xBrA8fBwhui9
…ate (#555) Review-gate fixes for P5a: - Toast.svelte no longer imports $lib/voice/voice-state — that store pulls transcribeAudio/fetchVoiceConfig from $lib/api one transitive hop out, breaking ui-kit's presentational-only rule (plan §6.11) and blocking P5b from compiling Toast without recreating the ui app's voice subsystem. The ui app's root layout now mirrors voiceState.errorMessage into the notifications queue itself (same replace-in-place semantics), so the voice error surface is unchanged while ui-kit stays app-decoupled. - Extend the no-app-coupling hygiene test to forbid voice-state and any voice/ module-dir import so the coupling cannot be reintroduced. - Add a svelte-check based `check` script + standalone tsconfig to ui-kit: the raw-source package has no build step, so this is the ONLY TS coverage its ~86 source files get (consuming apps' vite builds strip types without checking; packages/ui's check only validates the public prop surface at usage sites). Probe-verified: an injected type error fails the script. $lib/* resolves types-only against packages/ui's implementations of the small app-provided contract (notifications / theme-state / focus-trap / error-messages — all import-free modules). - Wire the new gate into the workspace flow: root `check` runs it after ui:check, root `test` now includes packages/ui-kit (hygiene test runs in CI), ci.yml gets a "Type check UI kit" step, release.yml runs ui-kit:check alongside ui:check. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FetR7BttT1xBrA8fBwhui9
…med by workflow) Inert packages/client scaffold + partial transport red tests from the interrupted P5b test author; the resumed run completes them. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FetR7BttT1xBrA8fBwhui9
Completes the P5b red suite started in the interrupted WIP commit (transport request shaping / SSE parsing / health probe were already authored there; this adds the rest): - connections-store.test.ts: ConnectionEntry CRUD + active selection against the in-memory storage backend (plan §6.6) — persistence lives in the backend, locked entries immutable, remove clears a dangling active id. RED: missing src/lib/connections/index.ts (15 tests). - connections-seed.test.ts: runtime-config.json loading (own-origin '/runtime-config.json'; 404/offline/malformed -> null) + locked/default seeding semantics (idempotent upsert by id, config wins for locked, default never steals an explicit selection) + the offline read path. RED: missing src/lib/connections/index.ts (13 tests). - landing.test.ts: client landing choice — 0 connections -> /connections/new, else /chat (plan §6.5 pwa-static branch). RED: missing src/lib/resolve-landing.ts (3 tests). - purity.test.ts: dist-grep hygiene — the built bundle must contain no '@openpalm/lib' and no '/api/host' marker strings (§8.5/§8.10); fails loudly (never skips) while build output is absent. RED: build missing until `bun run client:build` lands (3 tests). The 3 source-hygiene tests in the same file (no @openpalm/lib dep in package.json, no src/lib/server/, no lib import in src) are CHARACTERIZATION tests that already pass on the inert scaffold and must stay green. Suite state: 64 tests across 7 files — 61 red for the right reason (missing feature modules / missing build), 3 green characterization. No production code. biome lint clean for packages/client. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FetR7BttT1xBrA8fBwhui9
P5b items 1-3 core modules for @openpalm/client (plan §6.6/§6.11): - src/lib/transport/index.ts — the ONE transport: direct fetch against a connection's OpenCode/guardian base URL with Basic (username defaults to 'openpalm', mirroring the host app's probeEndpoint)/Bearer/none auth, credentials 'omit' on every request (no cookies, ever), path-prefix-safe URL joining, session list/create + message send (OpenCode parts envelope), streaming-decode SSE frame parser, and a never-throwing health probe on the host app's RemoteStatus vocabulary. - src/lib/connections/index.ts — ConnectionEntry store behind a storage abstraction (in-memory + raw-IDB IndexedDB backends, same semantics); CRUD with locked (config-owned) entries immutable, persisted active selection, idempotent runtime-config seeding that never steals an explicit user selection, and loadRuntimeConfig() from the app's own origin (absent/offline/malformed -> null; offline boot never crashes). - src/lib/resolve-landing.ts — §6.5 client branch: 0 connections -> /connections/new, else /chat. Turns the P5b red suite green except the 3 dist-grep purity tests, which need the static build (next commit). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FetR7BttT1xBrA8fBwhui9
…ring (#555) P5b items 3-4 for @openpalm/client (plan §6.9 Slice A/§6.10/§6.11): - SvelteKit 2 + Svelte 5 app on adapter-static with SPA fallback (ssr=false, prerender=false) — no server runtime in the artifact, no src/lib/server/, no @openpalm/lib anywhere (§8.10; enforced by the purity dist-grep tests, now green against the built bundle). - Views adapted from packages/ui onto the transport + IndexedDB store (packages/ui keeps its own chat until parity): /chat (session list/ create + message send against the active connection; history + live SSE wiring follow with chat parity), /connections (client-side manager with per-connection direct health probes, locked/managed entries read-only), /connections/new -> /connections?new=1 alias. Root route redirects through resolveLanding(). Theme boot + Stillness tokens consumed from @openpalm/ui-kit (icons only — no ui-kit component that requires the $lib app contract, so the client supplies none yet). - Per-connection credential material lives in the secret store (IndexedDB meta area) under auth.secretRef — entries never carry credentials inline, and nothing host-scoped exists to hold (§8.10). - bin/serve.mjs: zero-dependency static file server — loopback bind by default, port via --port/PORT, SPA fallback to index.html, serves runtime-config.json from inside or beside the build, traversal-safe. - scripts/stamp-version.mjs writes .openpalm-client-version into the build (same delivery-stamp pattern as packages/ui). - Workspace wiring: root client:dev/build/check/test/serve scripts; client:check joins the root check chain. Runtime deps stay empty — the bundle is self-contained and the serve script is stdlib-only. - tsconfig gates the app source at 0 errors/0 warnings via svelte-check --fail-on-warnings; the bun:test suite is excluded from that program (bun module rules: bun:test builtin, .ts-extension imports) and is executed by bun test instead. All 64 package tests green (transport/store/landing/purity); checks 0/0 across ui, ui-kit, client; cli/lib/ui-vitest/electron at baseline parity. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FetR7BttT1xBrA8fBwhui9
Owner
|
@codex do a thorough code review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f9f0158de8
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
This was referenced Jul 8, 2026
Extract packages/client (@openpalm/client) + packages/ui-kit — thin static client app (Phase 5)
#555
Closed
Open
Open
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements Phase 5 of the UI runtime modes refactor: splits the monolithic UI into two independent artifacts—a shared presentational library (
@openpalm/ui-kit) and a new client-side SPA (@openpalm/client)—while refactoring the host UI (@openpalm/ui) to use the extracted kit and serve as the control plane only.Key Changes
New Packages:
packages/ui-kit/— Presentational components, icons, and design tokens (Stillness theme) extracted frompackages/ui. Consumed by both host and client apps.packages/client/— Standalone SPA (SvelteKit + static adapter) for direct connection management and chat. Runs on port 3890, talks directly to OpenCode/guardian endpoints with per-connection credentials. No proxy, no host APIs.Host UI Refactoring (
packages/ui):@openpalm/ui-kit; imports via@import '@openpalm/ui-kit/theme/tokens.css'/admin/*routes to/host/*and/api/admin/*to/api/host/*(Phase 4 cleanup)/admin/endpoints→/api/connections(connection management API)RuntimeContextv2 (lib/runtime-context.svelte.ts) — single source of truth for capability resolution (replaces legacy feature flags)resolveLanding()— unified landing matrix for root/splash routing (Phase 3)lib/server/features.ts— computes server-side runtime context from request eventAssistantTab.svelteto use new/api/assistant/personaendpointClient App (
packages/client):lib/transport/index.ts) — talks to connection's base URL with stored credentials/connections— connection CRUD + active selection (no host API calls)/chat— session list/create + message send via direct transport (SSE event wiring)lib/boot.ts) — loads runtime-config.json, seeds locked/default connections, probes healthbin/serve.mjs) spawned by CLI/ElectronCLI & Electron Integration:
openpalm admincommand (Phase 1.5, openpalm admin — serve the host UI from the CLI (host-ui mode, Phase 1.5) #556) — serves host UI withOP_ENABLE_ADMIN=1openpalm client-servecommand — runs client static server standalonemain.tsstarts both UI and client servers; window prefers client athttp://127.0.0.1:3890/chatTest Coverage:
Documentation:
docs/technical/ui-runtime-modes-plan.md— revised with Phase 5 as-built (Phases 5–6 re-scoped)docs/technical/phase-5-completion-guide.md— P5a–P5e completion checklistdocs/technical/ui-client-split-assessment.md— decision record for client splitdocs/technical/ui-route-map.md— host app route inventory (Phase 4)Implementation Details
ui-kit/src/lib/theme/tokens.css; both apps@importithttps://claude.ai/code/session_01FetR7BttT1xBrA8fBwhui9