# App shell — work-package 3: the grouped IA (round-14, operator-locked)

> Authored by the orchestrator (Fable), 2026-07-18. Ruled by
> `docs/DECISIONS.md` § "rounds 14/15/16 reacted" — **all eight round-14
> questions locked by the operator**; build exactly what
> `design/tiles/round-14/` draws, minus the explicitly-deferred parts below.
> The gate judges against THIS document.

## What this package is

The sidebar today is eight equal items in one flat list — the operator's
"crammed / unorganized" complaint. This package gives it shape. It is
**presentation-only on nav structure plus one route move**: no engine work,
no screen rebuilds.

## Locks (operator, 2026-07-18 — none of these are open)

1. Group labels **Monitor / Agent / Outreach**.
2. **Home solo above the groups; Settings solo pinned at the foot.**
3. **Agent is its own group with its sub-nav always visible** in the sidebar.
4. **Automation sits under Outreach.**
5. **Posts stays in Agent; Models moves to Settings** (Connections area).
6. Review-queue retirement is ruled but is NOT this package (agent scope owns
   the Activity end-state; see board).
7. **Phone drawer as drawn**: nested sub-nav inside the drawer list.
8. **Templates stays folded under Campaigns.**

## Stages

### Stage 1 — grouped sidebar + drawer (web only)

- `AppShell.tsx`: nav data gains groups (label + entries). Order: Home
  (solo) · MONITOR: Inbox, Analytics · AGENT: Agent + visible sub-nav
  (today's real entries — Overview, Activity, Posts, Behaviour, Knowledge,
  Playground, Models*, Triggers, Review* as they exist NOW; the four-entry
  slimming is scope-4's, tile-c is end-state reference only) · OUTREACH:
  Campaigns, Contacts, Automation · Settings (pinned foot).
  *Models leaves in Stage 2; Review leaves with the agent-scope Activity
  package — do not remove either here.
- Group labels styled per tile-a (uppercase, muted, letter-spaced), `--cm-*`
  tokens only, line-glyph icons (never emojis), no new entries in
  `routes/agent/styles.ts`.
- Drawer (≤768px): same grouped list nested per tile-b; overlay mechanics
  unchanged from appshell-scope-2.
- TDD: AppShell tests pin the group structure + order + Settings-at-foot +
  drawer nesting.

### Stage 2 — Models → Settings

- The Models screen mounts under Settings (tab or section beside
  Connections, matching how Settings pages are organized today); the
  `/agent/models` route redirects there; Agent sub-nav drops the entry.
- TDD: route redirect pinned; Settings renders the screen; no dead links
  (grep for `/agent/models` links).

## Gate bar

1. Web suite green (capped forks), typecheck/lint/build exit 0.
2. appshell-scope-2's bar NOT regressed: all nav screens still overflow-green
   at 320/375/768 (spot-sweep at least Home, Inbox, Activity, Posts,
   Settings, one Outreach screen).
3. The grouped shell matches the reacted tiles (labels, order, foot-pinned
   Settings, nested drawer) — screenshot evidence at 1440 and 375.
4. `/agent/models` redirect works; zero dead links to it.
5. No gate flips, no api/worker changes, no engine work. Commits
   pathspec-limited, pushed; pushed state builds.
6. Deploy/publish is the orchestrator's lane; the executor stops at
   built-and-pushed.

## Out of scope (recorded, not dropped)

- Agent sub-nav slimming to four → agent-scope-4.
- Review queue retirement + Activity one-list end-state → agent scope
  (DECISIONS "Activity end-state").
- Adopt-audit criticals (styling-system convergence, Settings split, focus
  ring) → separate packages; do not attempt here.

## Stage log

*(executor appends; gate verdict recorded here)*

### Stage 1 — grouped sidebar + drawer · DONE (`fa48c1d`, pushed to origin/rethink)

- `AppShell.tsx` nav restructured into the grouped IA: Home solo on top; groups
  MONITOR (Inbox, Analytics) · AGENT (Agent + its always-visible sub-nav, today's
  REAL nine entries mirroring `AgentNav.tsx` — Overview, Activity, Review queue,
  Posts, Triggers, Behaviour, Knowledge, Playground, Models) · OUTREACH
  (Campaigns, Contacts, Automation); Settings pinned solo at the foot, outside
  the Primary nav landmark. Line-glyph SVG icons per tile-a (`--cm-*` only, no
  emojis). Drawer (≤768px) nests the same grouped list per tile-b; overlay
  mechanics (scrim/Esc/scroll-lock/backdrop/close) unchanged. Role gating holds
  per entry AND per group (empty groups render nothing). No `routes/agent/styles.ts`
  change.
- TDD: `AppShell.test.tsx` rewritten red→green — pins group labels+order, the full
  flat link order (desktop + drawer), Agent-sub-nav nesting, Settings-at-foot,
  icon-is-svg/no-emoji, and per-role/-group gating. **17/17 green.**
- Oracles: web typecheck 0, lint 0 errors (7 pre-existing `any` warnings), web
  build ✓, `App.routes.test.tsx` 42/42.
- Screenshot evidence (local render of the real `tokens.css`+`appshell.css` with
  the exact AppShell markup): `plans/rethink/evidence/appshell-scope-3-desktop-1440.png`
  and `…-drawer-375.png` — both match tiles A/B.

### Stage 2 — Models → Settings · DONE (`10de2ec`, pushed to origin/rethink)

- Models mounts as a Settings tab beside Connections (`TAB_GROUPS` Connections →
  `["connections","models"]`); `/agent/models` → `Navigate to /settings?tab=models`;
  Settings opens on the deep-linked tab via a thin `SettingsRoute` wrapper in
  `App.tsx` (`asSettingsTab(?tab)` → `Settings initialTab`), keeping the URL read
  out of Settings (its unit test mounts router-free). Agent sub-nav (AppShell +
  `AgentNav.tsx`) drops the Models entry; Overview's "cost today" chip repointed
  to `/settings?tab=models`. Grep-clean: no nav links to `/agent/models` remain
  (only the redirect, its test assertion, and the `/api/agent/models*` backend
  endpoints the screen still calls).
- TDD: `Settings.test.tsx` (Models tab renders + deep-link lands), `App.routes.test.tsx`
  (`/agent/models`→`/settings` moved renders→redirects), `AgentNav.test.tsx` +
  `AppShell.test.tsx` (Models gone from sub-nav), `Overview.test.tsx` (chip href).
  Red→green.
- Oracles: web typecheck 0, lint 0 errors, web build ✓, **full web suite 606/606
  (82 files)** capped forks.

### Gate bar self-check (executor)

1. ✅ Web suite green (606/606 capped), typecheck/lint/build exit 0.
2. ⏳ appshell-scope-2 overflow spot-sweep at 320/375/768 — NOT re-run this seat
   (structural nav-only change; no content-screen layout touched). Flag for the
   gate's own sweep.
3. ✅ Grouped shell matches tiles A/B — screenshots at 1440 + 375 above.
4. ✅ `/agent/models` redirect works; zero dead nav links.
5. ✅ No gate flips, web-only, no engine work. Commits pathspec-limited, pushed;
   pushed tree built green locally.
6. ✅ Stopped at built-and-pushed; no deploy/publish.

**Observation for the gate (not a blocker):** Stage 1 puts the Agent sub-nav in
the sidebar per the kickoff, but `AgentLayout.tsx` still renders the in-page
`AgentNav` tab bar — so on an Agent screen the sub-nav shows twice (sidebar +
in-page). The kickoff scoped Stage 1 to `AppShell.tsx` + drawer and said "no
screen rebuilds", so removing the in-page `AgentNav` was left out of scope. The
mock (tile-b Agent page) shows no in-page tab bar, so the intended end-state is
likely sidebar-only. Recommend a follow-up (or agent-scope-4, which slims the
sub-nav anyway) retire the in-page `AgentNav`.

### GATE VERDICT (Fable, 2026-07-18): CONDITIONAL PASS — DEPLOYED LIVE

- Double-nav observation FIXED by the orchestrator (`f72d49f`): in-page
  AgentNav retired from AgentLayout; full web suite re-run by the gate seat
  **606/606** after the fix; web build ✓.
- Deployed + published: VPS pulled, web rebuilt, rsync'd; served bundle
  `index-DTHrVy7p.js`; health `{ok,db:true,redis:true}` (backend untouched).
- Bars 1, 3, 4, 5, 6: **PASS** (suite/typecheck/lint/build by both seat and
  gate; tiles matched by screenshot evidence; redirect verified by test;
  web-only pathspec commits; deploy done in the orchestrator lane).
- Bar 2 (appshell-scope-2 overflow spot-sweep at 320/375/768): **OPEN** —
  structural-only change argues no regression, but unverified; closes with
  the operator's authenticated phone glance or the next session's sweep.

### Bar 2 re-sweep (report-only seat, 2026-07-20)

Re-ran the 320/375/768 overflow spot-sweep against the grouped IA
(`fa48c1d`+`10de2ec`+`f72d49f`) on a local build + stub API (no login needed;
method: `corepack pnpm --filter @channels/web build`, `apps/web/dist` served
by a throwaway static+fixture Node server, chrome-devtools MCP emulate
viewports, oracle `document.documentElement.scrollWidth <=
document.documentElement.clientWidth` — **not** `window.innerWidth`, which a
vertical scrollbar can inflate and produce a false pass).

**All 15 current nav routes** (Home, Inbox, Analytics, the 8 Agent sub-nav
entries — overview/activity/review/posts/triggers/behaviour/knowledge/
playground, Campaigns, Contacts, Automation, Settings incl. the `?tab=models`
deep link) checked at 320/375/768: **clean, including every route the
appshell-scope-2 gate bar names by name** (Home, Inbox, Activity, Posts,
Settings, one Outreach screen = Campaigns) — **no regression from the grouped
IA**. Drawer verified live at 375: burger opens it, grouped list matches the
kickoff spec exactly (Home solo → MONITOR → AGENT with full sub-nav → OUTREACH
→ Settings pinned at foot), `so-scroll-lock` applied, no overflow while open.

**One NEW finding, unrelated to this package's nav change** (Agent Overview
was never in the original 14-route sweep list — it only became a directly-
reachable, separately-swept screen because scope-3 Stage 1 put it in the
visible sub-nav): at 320px, **Agent Overview overflows ~6px** — a channel
card's gate-status pill (e.g. "Canary (1)") renders as a non-wrapping
`nowrap` span that runs to x≈325.9 against a 320px viewport. 375 and 768
are clean. Screenshot:
`plans/rethink/evidence/appshell-scope-3-sweep/FAIL-agent-overview-320.png`.
This is a pre-existing content-layout gap (the Stage-2 sweep never covered
this screen), not a shell/nav regression — bar 2 as scoped to "no regression"
is satisfied, but this should be filed as a follow-up fix (the pill needs
`flex-wrap` or `min-width: 0` treatment, same house pattern as the other
Stage-2 fixes) before the route can be called clean end-to-end.

Evidence: `plans/rethink/evidence/appshell-scope-3-sweep/` —
`FAIL-agent-overview-320.png`, `PASS-drawer-open-375-grouped-nav.png` (the
record shot), plus `PASS-agent-posts-375-sidebar.png` and
`PASS-settings-375.png`.

Not verified: 200% text zoom and reduced-motion (out of this re-sweep's
scope, per the original scope-2 bar's own "not verified here" note); the
authenticated live-site phone glance (this was a local build, not the
published site).
