# Subagent detail modal — Phase 0 diagnosis

Branch: `feat/opencode-swappable-backend`
Date: 2026-05-27

## 1. Where the top-section sub-agent cards are rendered today

Live render path:
- `ui/src/ui/app-render.ts:88` → `renderAgentActivity` (live entry)
- `ui/src/ui/views/agent-activity.ts:803` `renderAgentActivity()`
- `ui/src/ui/views/agent-activity.ts:821` mounts the **`<subagent-activity-panel>`** custom element — *this* is the "top section" the user wants the modal wired to.
- The panel itself: `ui/src/ui/components/subagent-activity-panel.ts:130` (`@customElement("subagent-activity-panel")`).
- Card render: `subagent-activity-panel.ts:1135` `renderCard(agent, index)`.

The panel is **also** mounted from the chat view at `ui/src/ui/views/chat.ts:2040`. Wiring the modal at the panel level means both mounts get it.

## 2. What clicking a card currently does

`subagent-activity-panel.ts:1153`:
```ts
@click=${() => this.toggleExpanded(agent.runId)}
```

Clicking expands an inline `renderDetail(agent)` block inside the same card (run id, task text, result/error, timestamps). No modal, no navigation, no tasks list, no content pages.

## 3. Does the existing modal code ever render on the live page?

**Partially yes — but not from the panel.**

`agent-activity-dashboard.ts` already owns a full subagent detail modal:
- State: `_subagentModal` (line 77)
- Open: `openSubagentDetail()` (line 133)
- Close: `closeSubagentDetail()` (line 169)
- Render: `renderSubagentDetailModal()` (line 462)
- Mounted into the dashboard tree at line 1978 (`${_subagentModal ? renderSubagentDetailModal(...) : nothing}`).

The dashboard's own running-agent cards wire `onCardClick` to `openSubagentDetail` at line 1834 — so clicking a card *in the dashboard's "Running Agents" grid* (Section B inside `renderAgentDashboard`) does open the modal.

But the **top section** (`<subagent-activity-panel>`) lives **above** `renderAgentDashboard` in the live page (agent-activity.ts:821, vs. 946 for the dashboard), is a separate Lit element with its own Shadow DOM, and never calls `openSubagentDetail`. Its cards only toggle inline expansion.

The lane-collision artifact in `agent-activity.MIGRATION.md` matches: the dashboard rewrite landed in `agent-activity-dashboard.ts` because the "notification revamp agent could not touch `agent-activity.ts`". The modal infra got built but is only reachable from cards rendered inside the dashboard sub-tree.

## 4. Prior commits

Modal-related work on these files:
- `56cc420` — feat(subagents): progress tracking + registry recovery — the massive panel rewrite (1372 lines of `subagent-activity-panel.ts`). Adds inline `toggleExpanded` but NOT a modal hook.
- `75e60cc` — feat(activity-feed-focused-chat): add expandable chat modal — adds a *chat* modal (the right-drawer transcript composer in `agent-activity.ts`), not the subagent detail modal.
- `7cb64cc` — feat(activity): recently finished sessions feed.

The subagent **detail** modal (the one we want) was built inside `agent-activity-dashboard.ts` as part of the dashboard rewrite (lines 41–182, 462–655). It was never wired into the top panel.

The gateway handler `subagents.detail` already exists at `src/gateway/server-methods/subagents.handlers.ts:397`. It accepts either `runId` or `sessionKey` and returns `{ ok, run, project, task }` (project + task = the one linked task via `kanbanTaskId`).

## 5. The exact gap (wired vs not wired)

| Component | Wired? | Where |
| --- | --- | --- |
| `subagents.detail` RPC | ✅ | `subagents.handlers.ts:397` |
| Modal state + open/close | ✅ | `agent-activity-dashboard.ts:77,133,169` |
| Modal template | ✅ | `agent-activity-dashboard.ts:462` |
| Modal mounted into dashboard sub-tree | ✅ | `agent-activity-dashboard.ts:1977-1979` |
| Dashboard "Running Agents" cards → modal | ✅ | `agent-activity-dashboard.ts:1834` |
| **Top-section `<subagent-activity-panel>` cards → modal** | ❌ | `subagent-activity-panel.ts:1153` — only `toggleExpanded` |
| Chat-view `<subagent-activity-panel>` cards → modal | ❌ | same panel, same issue |
| Modal shows **list of tasks** (plural) | ❌ | handler returns single `task`, not array |
| Modal shows **content pages** | ❌ | handler does not return content pages |
| Modal → switch to Kanban with task selected | ❌ | current "kanban" buttons just call `state.setTab("projects")` (line 470) with no task focus |
| Modal → switch to Content Pages with page open | ❌ | not implemented |
| Files-touched, Escape, backdrop close | ✅ | already in the dashboard modal |

## Chosen approach: (C) extract modal to its own component

Reasoning:
- The Lit Shadow DOM of `<subagent-activity-panel>` cannot reach into module-scope state in `agent-activity-dashboard.ts` cleanly via direct imports — Shadow DOM events do not auto-bubble to module-scope listeners. A `CustomEvent` with `bubbles + composed` is the correct decoupled bridge.
- Extracting the modal also lets the chat page get the same behavior with no edits to chat.ts beyond mounting one additional listener (or globally).
- Avoids the "two-modal drift" failure mode the acceptance criteria calls out: dashboard cards and panel cards both end up routed through the same `openSubagentDetailModal()` and same template.

Implementation outline (next phases):
1. New module `ui/src/ui/components/subagent-detail-modal.ts` — exports `openSubagentDetailModal`, `closeSubagentDetailModal`, `renderSubagentDetailModal`, `isSubagentDetailOpen`. Module-scope `_modalState`. Backdrop + Escape baked in.
2. Move the body of `renderSubagentDetailModal` / `openSubagentDetail` / `closeSubagentDetail` from `agent-activity-dashboard.ts` into the new module. Dashboard re-imports.
3. Extend `subagents.detail` to additionally return `tasks: TaskRef[]` (array, 0–N) and `contentPages: PageRef[]` (array, 0–N). Keep `task` (singular) for backwards compatibility.
4. Add `CustomEvent("infinibot:subagent-open-detail", { detail: { runId | sessionKey }, bubbles: true, composed: true })` dispatch from `subagent-activity-panel.ts` card click. Drop inline expansion as the primary behavior (the modal is the new place to see detail).
5. In `agent-activity.ts`, listen for the custom event on window/document and call `openSubagentDetailModal(client, ..., () => state.requestUpdate?.())`. Render the modal once at the top of the view template.
6. Modal `task` row click → `state.setTab("projects")`. Content-page row click → `state.setTab("content-pages")`. (Selecting the specific item inside that view is deferred — no existing controller hooks expose that today.)

## Phase 4 — verification status

Implementation is complete in source. Verification has known environment gaps:

- `pnpm exec tsc --noEmit` is clean for every file I touched; the four
  TS errors that remain in `agent-activity.ts:1023` (`spawn` notification type),
  `agent-activity-dashboard.ts` (`parentGoalId`), `codex.ts`, `auto-skill.ts`,
  `content-builder.ts`, `app-render.ts` (Learn/Apps host shapes, video flags)
  are pre-existing — not introduced by this work.
- `pnpm build` in `ui/` fails at `masonry-grid.ts` because
  `grid-panels/{database,projects,tasks}-panel.ts` are referenced but not
  present on disk. This is A3's lane (per the task brief). 581/582
  modules transform cleanly, including the new modal component.
- Vitest is configured with `@vitest/browser-playwright`. In this
  environment a playwright-driven test hangs >180s at launch (verified
  against the pre-existing `src/ui/lib/lru-cache.test.ts` too — same
  symptom on code I did not touch). The `subagent-detail-modal.test.ts`
  covers the open/close lifecycle, body-overflow restoration, loading
  skeleton, Escape close, and post-resolve render with tasks +
  contentPages rows. It will run once the playwright launch issue is
  resolved.
- The running gateway at `http://localhost:18789/` serves a stale
  pre-built bundle (`assets/index-AVIX6ZPx.js`). Without a fresh build
  (blocked above) the changes can't be visible in the operator's
  browser. The UIControlBus has no `agent_activity` view registered, so
  `ui_control dispatch op=select_subagent` is also not available — that
  registration is a separate scope (would need
  `UIControlBus.register({ view: "agent_activity", ... })` added to
  `agent-activity.ts` to expose state + actions).
