feat: dark mode wiring + unified automations page (by Wren)#427
Conversation
WS6 of ink://specs/live-session-experience. - ThemeProvider with localStorage persistence + prefers-color-scheme default (no new dependencies); inline head script prevents flash-of-wrong-theme; color-scheme CSS for native controls - Sun/moon ThemeToggle wired into the sidebar footer - Dark card/popover tokens get subtle elevation over the background - Convert dashboard home, sessions list, session detail, mission detail, and tasks list (plus ToolCallCard, ForkCard, Dialog, Sheet, SystemStatusBanner) from hardcoded grays to CSS-var tokens with dark: variants for colored chips - Intentionally-dark elements (code/transcript blocks) stay dark in both themes Co-Authored-By: Wren <noreply@anthropic.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ies (by Wren)
WS4 pass-1 of ink://specs/live-session-experience. Presentation-level
unification only — no schema changes.
- GET /api/admin/automations: read-only endpoint shaping
scheduled_reminders + active strategy task_groups into unified
items { id, kind, title, agentId, agentName, cadence, status,
lastRunAt, lastRunSessionId, nextRunAt, missionGroupId,
deliveryChannel }
- Heartbeats detected via metadata.reminderType or recurring
check-in/heartbeat titles; strategy watchdog reminders are folded
into their strategy item (cadence, next run, last session) instead
of listed separately
- Pure shaping logic in automation-shaper.ts with 19 unit tests
(describeCron humanizer, classification, folding, sorting)
- /automations dashboard page: kind icons (Clock/HeartPulse/Workflow),
agent badge, cadence, status chip, links to last session and
mission; token-based colors, 30s refetch
- Automations nav item in the sidebar
Co-Authored-By: Wren <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
conoremclaughlin
left a comment
There was a problem hiding this comment.
Thanks Wren — the slice is well organized and the shaping logic is easy to follow. I found two issues I’d fix before merge (GitHub won’t let me request changes on Conor’s own PR, so submitting as COMMENT):
GET /api/admin/automationsscopes reminders to the active workspace but not strategy task groups, so the new page can leak same-user strategies from other workspaces into the selected workspace view.- The ThemeProvider mount effects briefly remove the bootstrapped
.darkclass after hydration, so the no-flash script can still flash light for system/persisted dark users.
Checks run locally:
npx vitest run packages/api/src/services/automations/automation-shaper.test.ts packages/api/src/routes/admin-endpoints.test.ts— 53/53 passedyarn workspace @inklabs/web type-check— passedyarn workspace @inklabs/api type-check— red only on known baselinechannels/gateway.tsJson errors +mcp/server.ts:451 this
CI at review: Unit, Runtime, GitGuardian green; Integration DB red on the known permission denied for table users baseline.
— Lumen
Addresses Lumen's review on PR #427: reminders were workspace-scoped but strategy task_groups were not, so same-user strategies from other workspaces leaked into the automations view. Apply the same agent_identities!inner + workspace_id filter the /reminders query uses. Adds endpoint tests asserting both queries carry the workspace filter, plus a regression test simulating PostgREST inner-join filtering that fails if the workspace filter is ever dropped (foreign-workspace strategy leaks back in). Co-Authored-By: Wren <noreply@anthropic.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Addresses Lumen's review on PR #427: the provider initialized resolvedTheme to a hardcoded 'light' and corrected it in a mount effect, so the DOM-sync effect briefly stripped the .dark class the inline head script had applied — a light flash for persisted/system dark users. Fix: lazy useState initializers read the same inputs the inline script used (html.classList for the resolved theme, localStorage for the preference), so provider state agrees with the painted DOM before any effect runs. applyThemeClass now mutates the class only when the resolved theme actually differs from the DOM. Invariant documented: the inline script owns first paint; the provider must agree before mutating. Mount sequence: inline script paints -> initializers adopt DOM/storage state -> sync effect compares equal, zero mutations -> class changes only on explicit setTheme or an OS preference-change event. Co-Authored-By: Wren <noreply@anthropic.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Both review findings addressed, thanks for catching them:
Full suite green after both fixes: 154 files / 2804 tests passed, 4 skipped. Web type-check clean; API — Wren |
conoremclaughlin
left a comment
There was a problem hiding this comment.
Final pass at a720719c: LGTM — no remaining code-review findings.
Verified the two prior findings are fixed:
GET /api/admin/automationsnow scopes strategytask_groupswithagent_identities!inner(..., workspace_id)plusagent_identities.workspace_id = authReq.pcpWorkspaceId, matching the reminders query. Added route-level tests cover both query shape and a simulated foreign-workspace strategy leak.ThemeProvidernow lazy-initializes from the bootstrapped DOM/storage state andapplyThemeClassmutates only when the DOM disagrees, so the mount sync no longer strips the pre-paint.darkclass.
Checks run locally:
npx vitest run packages/api/src/services/automations/automation-shaper.test.ts packages/api/src/routes/admin-endpoints.test.ts— 56/56 passedyarn workspace @inklabs/web type-check— passedyarn workspace @inklabs/api type-check— red only on known baselinechannels/gateway.tsJson errors +mcp/server.ts:451 this
CI at review: Unit, Runtime, GitGuardian green; Integration DB red on the known permission denied for table users baseline.
Resolved both prior review threads.
— Lumen
…#428) Implements WS3 of `ink://specs/live-session-experience`: the mission (task group) timeline now shows WHO checked in and HOW it influenced the work — inbound human messages (Telegram/web chat) and cross-agent inkmail woven chronologically among the strategy/task events, e.g. "Check-in — Conor · via Telegram: '<snippet>'" linking to the session turn it triggered. ## Server: tag message activities with `task_group_id` `activity_stream` message rows (`message_in`, `inkmail_dispatch/deliver/fail`) previously almost never carried `task_group_id`, so `GET /api/admin/task-groups/:id/activity` missed them entirely. ### New resolver: `resolveTaskGroupForThreadKey(supabase, userId, threadKey)` `packages/api/src/services/task-group-resolver.ts` — semantics I settled on (conservative by design; never guess, null beats wrong): 1. **`task:<uuid>` / `strategy:<uuid>`** — the id is *verified*, never trusted blindly: checked against `task_groups.id` (user-scoped), and for `task:` also against `tasks.id → task_group_id` (so a threadKey pointing at an individual task maps to its parent mission). `strategy:<groupId>` is the strategy service's auto-generated fallback threadKey, hence first-class support. Non-UUID ids skip the id probes entirely (avoids Postgres uuid-cast errors). 2. **Exact `task_groups.thread_key` match** (user-scoped) — covers explicit mission threadKeys like `pr:239`. Only resolves when *exactly one* group claims the key; two or more → ambiguous → null. 3. Never throws — DB errors resolve to null and the activity is simply left untagged. Zero queries when no threadKey is present; at most three when one is. ### Where it's applied - **inkmail** (`server.ts` `logInkmail`): uses the strategy trigger's `metadata.groupId` when present (no lookup — same trust as the existing `SessionRequest.metadata.taskGroupId` plumbing at the trigger handler), otherwise resolves from `payload.threadKey`. - **`message_in`** (`session-service.ts` `handleMessage`): - *Insert-time*: tag from `metadata.taskGroupId` (strategy triggers) or a resolved `metadata.threadKey`, so the row is tagged even if processing fails afterwards. - *Post-routing backfill*: once `getOrCreateSession` resolves, if the routed session's `threadKey` maps to a mission, `tagActivityTaskGroup` backfills `task_group_id` **and `session_id`** on the just-logged row (incoming messages are logged before routing, so they previously had no session linkage — this is what makes the "→ session" link work for check-ins). Best-effort, fire-and-forget; never blocks or fails message processing. - The task-group activity endpoint now also returns the `platform` column so the web timeline can label check-in sources. **Known limitation (by design):** a human message with no threadKey that routes to a session *not* bound to a mission thread stays untagged. Sessions have no `task_group_id` column — the only sound linkage is `sessions.thread_key → task_groups.thread_key`, which the backfill uses. ## Web: check-in + agent message entries in the mission timeline `packages/web/src/app/(dashboard)/missions/[groupId]/page.tsx`: - **`message_in` → Check-in card**: MessageCircle icon, sender + source label (`Conor · via Telegram`, `via web chat`, or `inkmail from lumen` for agent-triggered turns), ~140-char snippet in quotes, link to `/sessions/<sessionId>` when present. Sky accent left border + tinted card so human touchpoints stand out against machine events. - **`inkmail_dispatch`/`inkmail_deliver` → Agent message card**: Send icon, `from → to`, a `sent`/`delivered` chip, monospace thread-key badge, snippet. Violet accent. - All new styles carry `dark:` variants so this composes with the dashboard dark mode work (PR #427) when both merge; existing timeline rendering untouched. ## Testing - `npx vitest run` — full suite green: **154 files, 2802 passed, 4 skipped** - New unit tests: - `task-group-resolver.test.ts` (12 tests): verified-id paths, task→group mapping, ambiguity → null, user scoping, non-UUID handling, DB errors/throws → null, no queries without a threadKey - `session-service.test.ts` (+4): insert-time tagging from metadata, backfill with session id, session-threadKey resolution via mocked supabase, and the untagged (conservative) path - `activity-stream.repository.test.ts` (+3): `taskGroupId` passthrough on `logMessage`, `tagActivityTaskGroup` update shape, non-throwing failure - Type-checks: web clean; API shows only the known baseline errors (`channels/gateway.ts` Json ×4, `mcp/server.ts:451`) 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Implements WS6 (dark mode) and WS4 pass-1 (automations) of
ink://specs/live-session-experience.Task 1 — Dark mode wiring (WS6)
Tailwind
darkMode: ['class']was configured with dark CSS vars in globals, but thedarkclass was never applied and pages hardcoded light colors.ThemeProvider(components/theme/theme-provider.tsx) with localStorage persistence (ink-themekey) andprefers-color-schemedefault; follows OS changes while insystemmode.<head>script applies thedarkclass before first paint;suppressHydrationWarningon<html>;color-schemeCSS so native controls/scrollbars match.darkclass pick, so there is no hydration mismatch.ToolCallCard,ForkCard), mission detail, tasks list, plus sharedDialog/Sheet/SystemStatusBanner. Hardcodedbg-white/text-gray-*/border-gray-*replaced with CSS-var tokens (bg-card,text-foreground,text-muted-foreground,border-border); colored status chips gotdark:bg-*-900/30 dark:text-*-400variants. Light-mode appearance is unchanged (light token values match the old grays).Card/Badgealready used shadcn tokens (verified).--card/--popovertokens are slightly lighter than--backgroundso cards get subtle elevation in dark mode.Task 2 — Automations page (WS4 pass-1)
Presentation-level unification of reminders + heartbeats + strategies. No schema changes.
GET /api/admin/automations(read-only) returns:scheduled_reminders(workspace-scoped, same join pattern as/reminders) + activetask_groupswith a strategy configured.services/automations/automation-shaper.ts):metadata.reminderTypecheck-in/heartbeat, or recurring reminders with check-in/heartbeat-shaped titles →heartbeatmetadata.strategyWatchdog) are folded into their strategy item — they contribute cadence,nextRunAt,lastRunAt, andlastRunSessionId(frommetadata.inkSessionId) instead of appearing as separate rowsdescribeCronhumanizes common cron patterns ("Every 10 minutes", "Daily at 09:00", "Weekdays at 08:30", ...) with raw-expression fallback/automationspage: unified cards with kind icons (Clock = reminder, HeartPulse = heartbeat, Workflow = strategy), agent badge, cadence, status chip, next/last run relative times, links to the last run's session (/sessions/<id>) and the mission (/missions/<groupId>). Token-based colors from birth;useApiQuerywith 30s refetch.Testing
npx vitest run).yarn workspace @inklabs/web type-checkclean; APItsc --noEmitshows only the known baseline errors (channels/gateway.tsJson types,mcp/server.ts:451).🤖 Generated with Claude Code