From 1b28ef85d73c3269abca3cbc8db2c86ea48bdbb3 Mon Sep 17 00:00:00 2001 From: Julius Olsson Date: Sun, 26 Jul 2026 21:00:06 +0200 Subject: [PATCH 1/5] docs(color-flags): plan the session-header flag surface Co-Authored-By: Claude Opus 5 (1M context) --- .../2026-07-26-session-header-color-flag.md | 489 ++++++++++++++++++ 1 file changed, 489 insertions(+) create mode 100644 docs/superpowers/plans/2026-07-26-session-header-color-flag.md diff --git a/docs/superpowers/plans/2026-07-26-session-header-color-flag.md b/docs/superpowers/plans/2026-07-26-session-header-color-flag.md new file mode 100644 index 00000000..975b7f79 --- /dev/null +++ b/docs/superpowers/plans/2026-07-26-session-header-color-flag.md @@ -0,0 +1,489 @@ +# Session Header Color Flag Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** A color-flagged agent shows that color as a chunk on the right end of its pane's session header, overlapping the status strip, so a flagged agent is spottable in the grid and not only in the Dispatch list. + +**Architecture:** The flag state already exists — `Settings.dispatchColorFlags` (sessionId → palette id), shipped in #603/#605. This adds a second *reader* of that state. The per-session lookup that `DispatchColorFlagStrip` performs inline is extracted into a `useColorFlag(sessionId)` hook so both surfaces resolve a flag identically; each surface keeps its own geometry, because a 10px alignment column and a 25% header chunk are different visual contracts. `PaneHeader` gains a `sessionId` prop and renders `PaneHeaderColorFlag` as a trailing flex child of its status row. + +**Tech Stack:** React 18, Zustand (`useAppStore`), Tailwind 4, Vitest + Testing Library (happy-dom `renderer` project). + +## Global Constraints + +- **Do not rename `Settings.dispatchColorFlags`.** It is a persisted settings key; renaming needs a `PERSIST_VERSION` migration for a purely cosmetic gain. +- **Do not rename the command id `dispatch.color-flag.set`.** Command ids key `Settings.commandVisibilityOverrides`; renaming silently drops a user's saved overrides. +- **Do not change the palette** (`DISPATCH_COLOR_FLAGS`) — ids, labels, and hex values stay exactly as they are. Persisted values are these ids. +- **No new test files.** Extend `src/renderer/src/workspace/dispatch/DispatchColorFlags.renderer.test.tsx`, which already owns this feature's coverage. +- **No absolute positioning, no negative margins, no painting over padding** for the chunk. `docs/plans_and_ideas/2026-07-23-color-flag-layout-follow-up.md` established that flags participate in flex layout; this plan follows the same rule. +- **Thick WHY comments** per `CLAUDE.md`, especially wherever this deviates from the Dispatch strip's contract. +- Renderer tests run with `NODE_ENV=test`. A leaked `NODE_ENV=production` loads prod React and breaks `act()`. + +--- + +## File Structure + +**Create:** +- `src/renderer/src/app-state/settings/useColorFlag.ts` — the one place that turns a `sessionId` into a `ColorFlag | undefined`. No geometry, no JSX. +- `src/renderer/src/workspace/tile-tree/TileLeaf/PaneHeaderColorFlag.tsx` — the header chunk: geometry + the unflagged-renders-nothing decision. + +**Modify:** +- `src/renderer/src/workspace/dispatch/DispatchColorFlagStrip.tsx` — consume the shared hook instead of selecting inline. +- `src/renderer/src/workspace/tile-tree/TileLeaf/PaneHeader.tsx` — `sessionId` prop, padding move, trailing chunk. +- `src/renderer/src/workspace/tile-tree/TileLeaf.tsx:620` — pass `sessionId`. +- `src/renderer/src/features/workspace/commands/dispatchColorFlagCommands.ts` — command description now names both surfaces. +- `src/renderer/src/app-state/settings/dispatchColorFlags.ts` — module header comment now names both surfaces. +- `src/renderer/src/workspace/dispatch/DispatchColorFlags.renderer.test.tsx` — new `describe` block for the header. + +**Why one shared hook and two components:** the *state* question ("is this session flagged, and in what color?") is identical everywhere and must never fork. The *layout* question is genuinely different per surface. Sharing the first and splitting the second is what keeps a future third surface from copy-pasting a store selector. + +--- + +### Task 1: Extract the shared flag lookup + +Pure refactor. Behavior identical; the existing three color-flag tests are the regression net and must stay green without modification. + +**Files:** +- Create: `src/renderer/src/app-state/settings/useColorFlag.ts` +- Modify: `src/renderer/src/workspace/dispatch/DispatchColorFlagStrip.tsx` +- Test: `src/renderer/src/workspace/dispatch/DispatchColorFlags.renderer.test.tsx` (existing cases, unchanged) + +**Interfaces:** +- Consumes: `colorFlagById(id: string | undefined): ColorFlag | undefined` and `type ColorFlag` from `@renderer/app-state/settings/dispatchColorFlags`; `useAppStore` from `@renderer/app-state/hooks`. +- Produces: `useColorFlag(sessionId: SessionId): ColorFlag | undefined` — consumed by Task 2. + +- [ ] **Step 1: Create the hook** + +```tsx +// src/renderer/src/app-state/settings/useColorFlag.ts +import { useAppStore } from '@renderer/app-state/hooks' +import { colorFlagById, type ColorFlag } from '@renderer/app-state/settings/dispatchColorFlags' +import type { SessionId } from '@renderer/workspace/types' + +/** + * Resolve a session's color flag, or `undefined` when it has none. + * + * WHY this is a hook and not each surface selecting inline: color flags now + * have more than one reader (the Dispatch trailing column and the pane header + * chunk), and the thing that must NEVER fork between them is the state + * question — which settings key is read, and how an unknown/stale persisted id + * degrades. Geometry deliberately stays with each surface, because a 10px + * alignment column and a 25% header chunk are different visual contracts that + * should be free to diverge. + * + * Returning the resolved `ColorFlag` rather than the raw id means every caller + * gets the same "unknown id → no flag" degradation for free: a palette entry + * deleted in a future version stops rendering everywhere at once instead of + * throwing at one call site and rendering `undefined` at another. + */ +export function useColorFlag(sessionId: SessionId): ColorFlag | undefined { + return useAppStore(state => colorFlagById(state.settings.dispatchColorFlags[sessionId])) +} +``` + +- [ ] **Step 2: Point the Dispatch strip at the hook** + +Replace the body of `DispatchColorFlagStrip.tsx` (keep the existing WHY comment block above the component verbatim — it documents the always-mounted-column decision, which is unchanged): + +```tsx +import { memo } from 'react' + +import { useColorFlag } from '@renderer/app-state/settings/useColorFlag' +import type { SessionId } from '@renderer/workspace/types' + +// WHY this is a real, always-mounted flex child rather than conditional +// absolute decoration: +// +// Color flags are an index column. Rows must reserve the same trailing 10px +// whether they are flagged or not so titles and compact labels align while the +// user scans vertically. The first implementation painted over right padding, +// which made the rich list look correct in isolation but gave Tiled Dispatch no +// reusable width contract at all. Keeping the settings lookup and the geometry +// in one component makes every Dispatch selector opt into the same invariant. +// +// The settings lookup itself now lives in `useColorFlag` because the pane +// header reads the same state with different geometry; only the geometry below +// is Dispatch-specific. +export const DispatchColorFlagStrip = memo(function DispatchColorFlagStrip({ + sessionId, +}: { + sessionId: SessionId +}) { + const colorFlag = useColorFlag(sessionId) + + return ( +