feat(client): agent activity history UI — "what Carol changed" #351

Merged
james merged 1 commit from 350-activity-history-ui into main 2026-06-29 12:14:45 +00:00
Owner

Makes the audit trail (#348, GET /api/agent/audit) user-visible: api-client hooks + a paginated, read-only activity screen showing every change Carol made, newest first. Undo is a separate follow-up.

What's in it

  • @carol/api-clientuseAuditLog() + useAuditLogInfinite() over /api/agent/audit, mirroring the conversations hooks; keys.auditLog.
  • lib/activity/describeAuditEvent.ts — pure, tested mapper from an event's verb_noun action to a relative i18n verb key + the raw entity type.
  • app/(app)/activity.tsx — a dedicated routable screen (out of the fixed sidebar, like notes.tsx), reached via a card in Settings. Cursor "load more", loading/error/empty branches, DS tokens, Carol's voice.
  • account.tsx — an ActivityCard linking to /activity.
  • i18n — new activity namespace (verb fragments + entity nouns); es.json untouched.

Two issues I caught and fixed during review (beyond the agent's output)

  1. Rendering bug — full keys double-namespaced. The helper returned activity.verb.create but the screen renders via useTranslation("activity"), so t("activity.verb.create") resolved to the literal key string on screen (verified against the real i18n config — the node tests only assert the helper's return value, so they didn't catch it). Fixed by returning a relative key (verb.create).
  2. Snake_case leaking into the UI. The entity noun was the raw entityType, so person_organization would render as "I created a person_organization". Now localized via an activity.entity.* catalog (person→"contact", person_note→"note", person_organization→"connection", profile→"profile") with a de-snaked fallback for future types. Verified end-to-end against the real i18n config:
    • person → "I created a contact" · person_note → "I added a note" · person_organization → "I deleted a connection" · profile → "I updated a profile" · unlabeled job → "I created a job" (fallback).

Verification (local)

  • @carol/api-client: typecheck ✓ · lint ✓ · check ✓ (no spec/codegen change) · test ✓ 42
  • @carol/client: typecheck ✓ · lint ✓ · test ✓ 154 (incl. updated describeAuditEvent cases)
  • end-to-end i18n resolution checked against the real config (above); en.json valid
  • semgrep (CI pack set) ✓ 0 findings on the new/changed files

⚠️ Needs maintainer verification (headless limit)

Client vitest is node-env — the visual screen (list, load-more, Settings card, navigation) needs a real PWA/device check.

Out of scope (follow-ups)

  • Undo (backend reversal endpoint + an undo button here).
  • before/after diff expansion; filtering. (Note: the empty/lede copy says "nothing here can be undone" — the undo ticket should update that line.)

Part of epic #47; makes #348 visible.

Closes #350

🤖 Generated with Claude Code

Makes the audit trail (#348, `GET /api/agent/audit`) user-visible: api-client hooks + a paginated, read-only activity screen showing every change Carol made, newest first. Undo is a separate follow-up. ## What's in it - **`@carol/api-client`** — `useAuditLog()` + `useAuditLogInfinite()` over `/api/agent/audit`, mirroring the conversations hooks; `keys.auditLog`. - **`lib/activity/describeAuditEvent.ts`** — pure, tested mapper from an event's `verb_noun` action to a relative i18n verb key + the raw entity type. - **`app/(app)/activity.tsx`** — a dedicated routable screen (out of the fixed sidebar, like `notes.tsx`), reached via a card in Settings. Cursor "load more", loading/error/empty branches, DS tokens, Carol's voice. - **`account.tsx`** — an `ActivityCard` linking to `/activity`. - **i18n** — new `activity` namespace (verb fragments + entity nouns); `es.json` untouched. ## Two issues I caught and fixed during review (beyond the agent's output) 1. **Rendering bug — full keys double-namespaced.** The helper returned `activity.verb.create` but the screen renders via `useTranslation("activity")`, so `t("activity.verb.create")` resolved to the **literal key string** on screen (verified against the real i18n config — the node tests only assert the helper's return value, so they didn't catch it). Fixed by returning a **relative** key (`verb.create`). 2. **Snake_case leaking into the UI.** The entity noun was the raw `entityType`, so `person_organization` would render as "I created a person_organization". Now localized via an `activity.entity.*` catalog (`person`→"contact", `person_note`→"note", `person_organization`→"connection", `profile`→"profile") with a de-snaked fallback for future types. Verified end-to-end against the real i18n config: - `person` → "I created a contact" · `person_note` → "I added a note" · `person_organization` → "I deleted a connection" · `profile` → "I updated a profile" · unlabeled `job` → "I created a job" (fallback). ## Verification (local) - `@carol/api-client`: typecheck ✓ · lint ✓ · `check` ✓ (no spec/codegen change) · test ✓ 42 - `@carol/client`: typecheck ✓ · lint ✓ · test ✓ 154 (incl. updated `describeAuditEvent` cases) - end-to-end i18n resolution checked against the real config (above); `en.json` valid - semgrep (CI pack set) ✓ 0 findings on the new/changed files ## ⚠️ Needs maintainer verification (headless limit) Client vitest is `node`-env — the **visual screen** (list, load-more, Settings card, navigation) needs a real PWA/device check. ## Out of scope (follow-ups) - **Undo** (backend reversal endpoint + an undo button here). - before/after diff expansion; filtering. (Note: the empty/lede copy says "nothing here can be undone" — the undo ticket should update that line.) Part of epic #47; makes #348 visible. Closes #350 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(client): agent activity history UI — "what Carol changed"
All checks were successful
Commits / Conventional Commits (pull_request) Successful in 6s
PR / Static analysis (pull_request) Successful in 2m46s
PR / Lint (pull_request) Successful in 3m40s
PR / pnpm audit (pull_request) Successful in 4m1s
PR / OpenAPI (pull_request) Successful in 4m8s
PR / Client (web export smoke) (pull_request) Successful in 4m20s
PR / Build (pull_request) Successful in 4m32s
PR / OSV-Scanner (pull_request) Successful in 1m51s
PR / Typecheck (pull_request) Successful in 4m39s
PR / Package age policy (soft) (pull_request) Successful in 53s
PR / Test (postgres) (pull_request) Successful in 5m2s
PR / Test (sqlite) (pull_request) Successful in 5m7s
Secrets / gitleaks (pull_request) Successful in 49s
PR / E2E (Playwright) (pull_request) Successful in 6m8s
PR / Trivy (image) (pull_request) Successful in 2m40s
PR / Coverage (soft) (pull_request) Successful in 2m24s
c4569eb87d
Make the audit trail (#348, GET /api/agent/audit) user-visible: a
paginated, read-only activity screen showing every change Carol made,
newest first. Undo is a separate follow-up.

- @carol/api-client: useAuditLog() + useAuditLogInfinite() over
  /api/agent/audit, mirroring the conversations hooks; keys.auditLog.
- lib/activity/describeAuditEvent.ts: pure, tested mapper from an audit
  event's verb_noun action to a relative i18n verb key + the raw entity
  type.
- app/(app)/activity.tsx: a dedicated routable screen (out of the fixed
  sidebar, like notes.tsx), reached via a card in Settings. Localizes the
  entity noun via the activity catalog with a de-snaked fallback, so a
  type without a label never surfaces raw "person_organization".
- account.tsx: an ActivityCard linking to /activity.
- i18n: new activity namespace (verb fragments + entity nouns) in Carol's
  voice; es.json untouched.

The screen renders the description key with useTranslation("activity"),
so describeAuditEvent returns a RELATIVE key ("verb.create") — a full
"activity.verb.*" key would double-namespace and surface the raw key
string to users.

Closes #350

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

📊 Test coverage

Patch coverage: no testable lines changed.

Overall (app/, lib/, db/, excluding UI per ADR-0019):

Metric Value Soft target
Lines 79.3% ≥ 50%
Branches 71.1% ⚠️ ≥ 75%
Functions 80.3% informational

Soft thresholds per ADR-0019. Coverage is informational and does not block merge.

<!-- coverage-comment --> ## 📊 Test coverage **Patch coverage:** no testable lines changed. **Overall** (`app/`, `lib/`, `db/`, excluding UI per ADR-0019): | Metric | Value | Soft target | |---|---|---| | Lines | 79.3% ✅ | ≥ 50% | | Branches | 71.1% ⚠️ | ≥ 75% | | Functions | 80.3% | informational | Soft thresholds per [ADR-0019](docs/adr/0019-coverage-soft-targets.md). Coverage is informational and does not block merge.
james merged commit f278e799cb into main 2026-06-29 12:14:45 +00:00
james deleted branch 350-activity-history-ui 2026-06-29 12:14:46 +00:00
Sign in to join this conversation.
No description provided.