Files
kandev/docs/plans/message-queue-pin/task-01-frontend-pin.md
T
2026-08-24 11:09:03 +00:00

121 lines
6.5 KiB
Markdown

---
id: "01-frontend-pin"
title: "Frontend pin control and open-state persistence"
status: done
wave: 1
depends_on: []
plan: "plan.md"
spec: "../../specs/ui/requirements/message-queue-pin.md"
---
# Task 01: Frontend pin control and open-state persistence
- **Acceptance:**
1. `useQueuePinned(sessionId)` returns a per-session localStorage-backed
boolean defaulting to `false`, following the `useLocalStorageBoolean`
shape (storage key `kandev:queue:pinned:<session_id>:v1`, sync event
`kandev:queue:pinned-changed`); `null` session degrades to `false` with
no storage access.
2. The expanded queue panel header shows a `queue-pin` toggle between
**Clear all** and **X** with `aria-pressed`, localized title/aria-label,
and the sibling touch sizing; clicking it toggles and persists the
session pin.
3. When pinned with queued entries, `QueueAffordance` mounts with the panel
open (no chip); unpinned mounts stay collapsed; zero entries always hide
the panel; session switches follow the target session's pin.
4. All new copy goes through `t()` with keys added to en, pseudo, pt-pt,
and zh-cn `chat.json`; `pnpm run i18n:check` and `i18n:ratchet` pass.
- **Verification** (one block rooted at `apps`):
```bash
cd apps && pnpm install --frozen-lockfile
cd apps/web && pnpm run typecheck && pnpm run lint
cd apps && pnpm --filter @kandev/web test -- hooks/use-queue-pinned.test.ts components/task/chat/queued-ghost-list.test.tsx
cd apps/web && pnpm run i18n:check
```
Note: `useQueuePinned` is mocked in the component test via a localStorage
mock (see `hooks/local-storage-mock.test-helpers`); the hook has its own
unit test.
- **Files likely touched:**
- `apps/web/hooks/use-queue-pinned.ts` (new) + `use-queue-pinned.test.ts` (new)
- `apps/web/components/task/chat/queued-ghost-panel-header.tsx`
- `apps/web/components/task/chat/queued-ghost-list.tsx`
- `apps/web/components/task/chat/queued-ghost-list.test.tsx`
- `apps/web/src/locales/{en,pseudo,pt-pt,zh-cn}/chat.json`
- **Dependencies:** None.
- **Parallelism:** sequential.
- **Inputs:** spec `What` + `Data model` + `Failure modes` sections;
plan Frontend + Tests sections. Patterns: `apps/web/hooks/use-local-storage-boolean.ts`
(and its test), existing header buttons in `queued-ghost-panel-header.tsx`,
render-phase state adjustment in `useQueuePanelOpenState`.
- **Output contract:** summary, files changed, exact commands + outcomes,
task/plan status update. Confirm `IconPin`/`IconPinned` exist in the
installed `@tabler/icons-react`; if not, use the closest existing
filled/outline pair and note it.
## Results
- `cd apps && pnpm --filter @kandev/web test -- hooks/use-queue-pinned.test.ts components/task/chat/queued-ghost-list.test.tsx components/task/chat/queued-ghost-pin.test.tsx` → 3 files, 56 tests passed.
- `cd apps/web && pnpm run typecheck` → passed (`tsc --noEmit` clean).
- `cd apps/web && pnpm run lint` → clean (`eslint --max-warnings 0`, 0 warnings).
- `cd apps/web && pnpm run i18n:check` → passed (keys in sync, no em dashes, no inline plurals).
- `cd apps/web && pnpm run i18n:ratchet` → passed (0 added + 2 modified files clean).
- `IconPin`/`IconPinned` confirmed present in installed `@tabler/icons-react` (rendered SVG check).
- Files changed beyond the planned list: `useQueuePanelOpenState` extracted to
`apps/web/components/task/chat/use-queue-panel-open-state.ts` (lint 600-line
cap); pin tests live in a dedicated `queued-ghost-pin.test.tsx` (split-test
convention, cf. `pr-ci-popover.automation.test.tsx`); `useQueuePinned` also
exposes `toggle` (swallows persistence failures) to keep `QueueAffordance`
under the 100-line function cap. Spec updated: a pinned panel opens when
entries arrive asynchronously after mount (initial open state is `pinned`,
not gated on entry count; the render path gates on `hasEntries`).
- Security/trust boundaries: `None`. External side effects: `None`.
### Adversarial review loop (10-luna-review-fix)
Three review rounds produced findings; all fixed and committed; round 4 found
none and the loop stopped:
1. `dabb6c80e` — open the panel when a storage-synced pin flips true
(multi-tab) with entries present; corrected the pseudo-locale `U` glyph
(`Ū` → `Ũ`) in `unpinQueuedMessages`.
2. `8fb2a3de3` — reopen a pinned queue on the empty→nonempty entryCount
transition (async load after session switch, storage pin on an empty
queue, later queue after a full drain); X-collapsed nonempty pinned queues
are not reopened by ordinary count changes.
3. `fdaf84ee9` — initialize `isOpen` as `pinned && entryCount > 0` so an
empty pinned queue starts collapsed; unpinning it before delayed entries
arrive leaves it collapsed instead of leaking an open panel.
Unit tests grew from 56 to 63 (pin spec split across three describes to stay
under the 100-line per-function cap). Desktop + mobile E2E re-run green after
the fixes.
### Desktop-only + PR review round (post-PR)
- `41f376948` — pin is desktop-only: the header hides the toggle on phone
viewports (`useResponsiveBreakpoint`), mobile E2E now asserts the pin is
absent while Clear all/close stay touch-sized, spec updated.
- Review fixes (uncommitted at time of writing, landed with this round):
- `useQueuePanelOpenState` treats the pin as off on mobile so a pin stored
at desktop width never auto-opens the panel on phone viewports (Codex
P2; regression test added).
- `useQueuePinned.toggle` now keeps the current-view pin flipped via an
in-memory fallback when `localStorage.setItem` fails (CodeRabbit
functional-correctness finding), cleared by the next successful write.
- Renamed the mis-described session-switch test in the pin spec (Claude
blocker).
- Final unit count 66/66; mobile E2E 3/3 (incl. pin-hidden-on-mobile),
desktop pin E2E 2/2, typecheck + lint + i18n clean.
### Desktop-only pin (post-review product decision)
The pin is now a desktop-only control per explicit user request: the header
gates the button on `useResponsiveBreakpoint().isMobile` (width < 768px), so
phone viewports render the queue panel without the pin while Clear all /
Send now / Run next / collapse remain. Unit tests: two new cases (pin hidden
on mobile, rendered on desktop) via the `use-responsive-breakpoint` mock —
65 total. Mobile E2E: the pin-present navigation test was replaced with a
desktop-only guard asserting `queue-pin` has count 0 and the remaining header
controls stay touch-sized; desktop pin E2E unchanged (2/2). Spec updated
(`What`, Scenarios, Out of scope) with the mobile exclusion.