Skip to content

feat: dark mode wiring + unified automations page (by Wren)#427

Merged
conoremclaughlin merged 4 commits into
mainfrom
wren/feat/dark-mode-automations
Jul 4, 2026
Merged

feat: dark mode wiring + unified automations page (by Wren)#427
conoremclaughlin merged 4 commits into
mainfrom
wren/feat/dark-mode-automations

Conversation

@conoremclaughlin

Copy link
Copy Markdown
Owner

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 the dark class was never applied and pages hardcoded light colors.

  • Theme system with no new dependencies: small ThemeProvider (components/theme/theme-provider.tsx) with localStorage persistence (ink-theme key) and prefers-color-scheme default; follows OS changes while in system mode.
  • No flash-of-wrong-theme: inline <head> script applies the dark class before first paint; suppressHydrationWarning on <html>; color-scheme CSS so native controls/scrollbars match.
  • Sun/moon toggle (lucide) wired into the sidebar footer — renders both icons and lets the dark class pick, so there is no hydration mismatch.
  • Token conversion of the highest-traffic surfaces: dashboard home, sessions list, session detail (incl. ToolCallCard, ForkCard), mission detail, tasks list, plus shared Dialog/Sheet/SystemStatusBanner. Hardcoded bg-white / text-gray-* / border-gray-* replaced with CSS-var tokens (bg-card, text-foreground, text-muted-foreground, border-border); colored status chips got dark:bg-*-900/30 dark:text-*-400 variants. Light-mode appearance is unchanged (light token values match the old grays). Card/Badge already used shadcn tokens (verified).
  • Intentionally-dark elements (code/transcript blocks, terminal panes) stay dark in both themes.
  • Dark --card/--popover tokens are slightly lighter than --background so 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:
    { automations: [{ id, kind: 'reminder'|'heartbeat'|'strategy', title, agentId, agentName,
                      cadence, status, lastRunAt, lastRunSessionId, nextRunAt,
                      missionGroupId, deliveryChannel }] }
    
    Sources: scheduled_reminders (workspace-scoped, same join pattern as /reminders) + active task_groups with a strategy configured.
  • Heuristics (pure logic in services/automations/automation-shaper.ts):
    • metadata.reminderType check-in/heartbeat, or recurring reminders with check-in/heartbeat-shaped titles → heartbeat
    • strategy watchdog reminders (metadata.strategyWatchdog) are folded into their strategy item — they contribute cadence, nextRunAt, lastRunAt, and lastRunSessionId (from metadata.inkSessionId) instead of appearing as separate rows
    • describeCron humanizes common cron patterns ("Every 10 minutes", "Daily at 09:00", "Weekdays at 08:30", ...) with raw-expression fallback
  • /automations page: 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; useApiQuery with 30s refetch.
  • "Automations" nav item added to the sidebar (Platform group).

Testing

  • 19 new unit tests for the shaper (cron humanizer, classification, watchdog folding, sorting) — no DB required.
  • Full suite: 154 files / 2801 tests passed, 4 skipped (npx vitest run).
  • yarn workspace @inklabs/web type-check clean; API tsc --noEmit shows only the known baseline errors (channels/gateway.ts Json types, mcp/server.ts:451).

🤖 Generated with Claude Code

conoremclaughlin and others added 2 commits July 4, 2026 04:06
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 conoremclaughlin left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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):

  1. GET /api/admin/automations scopes 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.
  2. The ThemeProvider mount effects briefly remove the bootstrapped .dark class 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 passed
  • yarn workspace @inklabs/web type-check — passed
  • yarn workspace @inklabs/api type-check — red only on known baseline channels/gateway.ts Json 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

Comment thread packages/api/src/routes/admin.ts
Comment thread packages/web/src/components/theme/theme-provider.tsx
conoremclaughlin and others added 2 commits July 4, 2026 04:38
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>
@conoremclaughlin

Copy link
Copy Markdown
Owner Author

Both review findings addressed, thanks for catching them:

  1. Workspace scope leak (13ba70ca): the task_groups query in GET /api/admin/automations now uses the same agent_identities!inner(...) join + .eq('agent_identities.workspace_id', ...) filter as the reminders query, so same-user strategies from other workspaces can no longer leak in. Added endpoint tests in admin-endpoints.test.ts: one asserting both queries carry the workspace filter, and a regression test that simulates PostgREST inner-join filtering — if the workspace filter is ever dropped, the foreign-workspace strategy leaks back into the response and the test fails.

  2. Post-hydration theme flash (a720719c): the provider previously 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 script had applied. Now lazy useState initializers read the same inputs the inline script used (html.classList.contains('dark') for the resolved theme, localStorage for the preference), so provider state agrees with the painted DOM before any effect runs, and applyThemeClass only mutates the class when the resolved theme actually differs. Mount sequence: inline script paints → initializers adopt DOM/storage state → sync effect compares equal (zero mutations) → the class only changes on explicit setTheme or an OS preference-change event. The invariant (inline script owns first paint; provider must agree before mutating) is documented in the provider.

Full suite green after both fixes: 154 files / 2804 tests passed, 4 skipped. Web type-check clean; API tsc shows only the known baseline errors.

— Wren

@conoremclaughlin conoremclaughlin left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Final pass at a720719c: LGTM — no remaining code-review findings.

Verified the two prior findings are fixed:

  • GET /api/admin/automations now scopes strategy task_groups with agent_identities!inner(..., workspace_id) plus agent_identities.workspace_id = authReq.pcpWorkspaceId, matching the reminders query. Added route-level tests cover both query shape and a simulated foreign-workspace strategy leak.
  • ThemeProvider now lazy-initializes from the bootstrapped DOM/storage state and applyThemeClass mutates only when the DOM disagrees, so the mount sync no longer strips the pre-paint .dark class.

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 passed
  • yarn workspace @inklabs/web type-check — passed
  • yarn workspace @inklabs/api type-check — red only on known baseline channels/gateway.ts Json 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

@conoremclaughlin conoremclaughlin merged commit 3e55761 into main Jul 4, 2026
3 of 4 checks passed
conoremclaughlin added a commit that referenced this pull request Jul 4, 2026
…y Wren)

Keep #428's check-in/inkmail card branching, adopt #427's token-based
dark-ready classes in the generic entry path.

Co-Authored-By: Wren <noreply@anthropic.com>
conoremclaughlin added a commit that referenced this pull request Jul 4, 2026
…#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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant