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

Closed
opened 2026-06-29 11:47:24 +00:00 by james · 0 comments
Owner

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

Placement

The sidebar nav is a fixed union mirroring the design package, so this is not a new top-level nav item. It's a dedicated routable screen apps/client/app/(app)/activity.tsx (like notes.tsx — routable but out of the sidebar), reachable via a row/card in Settings (/account).

Scope

  • @carol/api-client (hooks/audit.ts): useAuditLog() (first page) + useAuditLogInfinite() over GET /api/agent/audit, mirroring useConversations/useConversationsInfinite exactly (cursor envelope { data, next_cursor }). Add keys.auditLog. Barrel-export; thin structural test.
  • Pure describe helper (apps/client/lib/activity/describeAuditEvent.ts): AuditEventDto -> { key, args } (an i18n key + interpolation args), NOT a hardcoded string. Derive from the action (verb_noun per ADR-0030) + entityType: map the verb (created/updated/deleted/added/removed/…) to an i18n key and render the entity type, with a sensible fallback for an unknown verb. This is the tested core — unit tests for create/update/delete across a couple of entities + the fallback.
  • activity.tsx: a paginated history screen mirroring notes.tsx (DS theme.tokens.*, RN primitives, KeyboardAwareScrollView or FlatList, loading/error/empty branches). Each event renders a one-line human description (via the helper + t()) + a relative/absolute timestamp; a "load more" affordance drives useAuditLogInfinite. Keep it thin — logic lives in the helper/hooks. (Before/after diff expansion is deferred polish.)
  • Link from account.tsx: a row/card ("Recent changes" / "What Carol changed") that navigates to /activity (expo-router Link/useRouter).
  • i18n: a new activity namespace in packages/i18n/messages/en.json (title, lede, empty/loading/error, load-more, the per-verb description keys, the account link label). Carol's voice: first person, sentence case, lead with the takeaway, no emoji. No hardcoded user-facing strings. es.json untouched.

Tests

  • describeAuditEvent unit tests (the valuable core): the common verbs + an unknown-verb fallback, asserting the returned { key, args }.
  • Thin structural hook test (exported from barrel; keys.auditLog registered), mirroring tests/contracts.test.ts.
  • Client vitest is node-env — the visual screen is verified manually by the maintainer.

Out of scope (follow-ups)

  • Undo (backend endpoint + the undo button here).
  • before/after diff expansion; filtering/search.

Part of epic #47; makes #348 visible.

Make the audit trail (#348, `GET /api/agent/audit`) user-visible: api-client hooks + a paginated activity screen showing every change Carol made, newest first. Read-only for now — **undo is a separate follow-up**. ## Placement The sidebar nav is a fixed union mirroring the design package, so this is **not** a new top-level nav item. It's a dedicated routable screen `apps/client/app/(app)/activity.tsx` (like `notes.tsx` — routable but out of the sidebar), reachable via a **row/card in Settings** (`/account`). ## Scope - **`@carol/api-client`** (`hooks/audit.ts`): `useAuditLog()` (first page) + `useAuditLogInfinite()` over `GET /api/agent/audit`, mirroring `useConversations`/`useConversationsInfinite` exactly (cursor envelope `{ data, next_cursor }`). Add `keys.auditLog`. Barrel-export; thin structural test. - **Pure describe helper** (`apps/client/lib/activity/describeAuditEvent.ts`): `AuditEventDto -> { key, args }` (an i18n key + interpolation args), NOT a hardcoded string. Derive from the `action` (`verb_noun` per ADR-0030) + `entityType`: map the verb (created/updated/deleted/added/removed/…) to an i18n key and render the entity type, with a sensible fallback for an unknown verb. This is the **tested core** — unit tests for create/update/delete across a couple of entities + the fallback. - **`activity.tsx`**: a paginated history screen mirroring `notes.tsx` (DS `theme.tokens.*`, RN primitives, `KeyboardAwareScrollView` or `FlatList`, loading/error/empty branches). Each event renders a one-line human description (via the helper + `t()`) + a relative/absolute timestamp; a "load more" affordance drives `useAuditLogInfinite`. Keep it thin — logic lives in the helper/hooks. (Before/after diff expansion is deferred polish.) - **Link from `account.tsx`**: a row/card ("Recent changes" / "What Carol changed") that navigates to `/activity` (expo-router `Link`/`useRouter`). - **i18n**: a new `activity` namespace in `packages/i18n/messages/en.json` (title, lede, empty/loading/error, load-more, the per-verb description keys, the account link label). Carol's voice: first person, sentence case, lead with the takeaway, no emoji. No hardcoded user-facing strings. `es.json` untouched. ## Tests - `describeAuditEvent` unit tests (the valuable core): the common verbs + an unknown-verb fallback, asserting the returned `{ key, args }`. - Thin structural hook test (exported from barrel; `keys.auditLog` registered), mirroring `tests/contracts.test.ts`. - Client vitest is `node`-env — the visual screen is verified manually by the maintainer. ## Out of scope (follow-ups) - **Undo** (backend endpoint + the undo button here). - before/after diff expansion; filtering/search. Part of epic #47; makes #348 visible.
james closed this issue 2026-06-29 12:14:46 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
james/carol#350
No description provided.