# Track Specification: MMA Agent Focus UX

## Overview
All MMA observability panels (comms history, tool calls, discussion) display
global/session-scoped data. When 4 tiers are running concurrently, their traffic
is indistinguishable. This track adds a `source_tier` field to every comms and
tool log entry at the point of emission, then adds a "Focus Agent" selector that
filters the Operations Hub panels to show only one tier's traffic at a time.

**Depends on:** `feature_bleed_cleanup_20260302` (Phase 1 removes the dead comms
panel duplicate; this track extends the live panel at gui_2.py:~3400).

## Current State Audit (as of 0ad47af)

### Already Implemented (DO NOT re-implement)
- **`ai_client._append_comms`** (`ai_client.py:136-147`): Emits entries with keys `ts`, `direction`, `kind`, `provider`, `model`, `payload`. No `source_tier` key.
- **`ai_client.comms_log_callback`** (`ai_client.py:87`): Module-level `Callable | None`. Tier 3 workers temporarily replace it in `run_worker_lifecycle` (`multi_agent_conductor.py:224-354`); Tier 2 (`conductor_tech_lead.py:6-48`) does NOT replace it.
- **`ai_client.tool_log_callback`** (`ai_client.py:91`): Module-level `Callable[[str,str],None] | None`. Never replaced by any tier — fires from whichever tier is active via `_run_script` (`ai_client.py:490-500`).
- **`self._tool_log`** (`gui_2.py:__init__`): `list[tuple[str, str, float]]` — stored as `(script, result, timestamp)`. Destructured in `_render_tool_calls_panel` as `script, result, _ = self._tool_log[i_minus_one]`.
- **`self._comms_log`** (`gui_2.py:__init__`): `list[dict]` — each entry is the raw dict from `_append_comms` plus `local_ts` stamped in `_on_comms_entry`.
- **`self.active_tier`** (`gui_2.py:__init__`): `str | None` — set by `_push_mma_state_update` when the engine reports tier activity. Tracks the *current* active tier but is not stamped onto individual log entries.
- **`run_worker_lifecycle` stream_id** (`multi_agent_conductor.py:299`): Uses `f"Tier 3 (Worker): {ticket.id}"` as `stream_id` for mma_streams. This is already tier+ticket scoped for the stream panels.
- **`_render_comms_history_panel`** (`gui_2.py:~3435`): Renders `self._comms_log` entries with `direction`, `kind`, `provider`, `model` fields. No tier column or filter.
- **`_render_tool_calls_panel`** (`gui_2.py:2989-3039`): Renders `self._tool_log` entries. No tier column or filter.
- **`disc_entries`** (`gui_2.py:__init__`, `_render_discussion_panel:gui_2.py:2482-2685`): List of dicts with `role`, `content`, `collapsed`, `ts`. Role values include "User", "AI", "Tool", "Vendor API" — MMA workers inject via `history_add` kind with a `role` field.

### Gaps to Fill (This Track's Scope)

1. **No `source_tier` on comms entries**: `_append_comms` never reads tier context. Tier 3 callbacks call the old callback chain but don't stamp tier info. Result: all comms from all tiers are visually identical in `_render_comms_history_panel`.

2. **No `source_tier` on tool log entries**: `_on_tool_log` receives `(script, result)` with no tier context. `_tool_log` is a flat list of tuples — no way to filter by tier.

3. **No `current_tier` module variable in `ai_client`**: There is no mechanism for callers to declare which tier is currently active. Both `run_worker_lifecycle` and `generate_tickets` call `ai_client.send()` without setting any "who am I" context that `_append_comms` could read.

4. **No Focus Agent UI widget**: No selector in Operations Hub or MMA Dashboard to choose a tier to filter on.

5. **No filter logic in `_render_comms_history_panel` or `_render_tool_calls_panel`**: Both render all entries unconditionally.

## Goals
1. Add `current_tier: str | None` module-level variable to `ai_client.py`; `_append_comms` reads and includes it as `source_tier` on every entry.
2. Set `ai_client.current_tier` in `run_worker_lifecycle` (Tier 3) and `generate_tickets` (Tier 2) around the `ai_client.send()` call; clear it in `finally`.
3. Change `_tool_log` from `list[tuple]` to `list[dict]` to support `source_tier` field; update all three access sites.
4. Add `self.ui_focus_agent: str | None = None` state var to `App.__init__`.
5. Add a "Focus Agent" combo widget in the Operations Hub tab bar header (or above it in MMA Dashboard).
6. Filter `_render_comms_history_panel` and `_render_tool_calls_panel` by `ui_focus_agent` when it is non-None.
7. Defer per-tier token stats (Phase 4 of TASKS.md intent) to a separate sub-track.

## Functional Requirements

### Phase 1 — Tier Tagging at Emission (ai_client.py + conductors)
- `ai_client.py`: Add `current_tier: str | None = None` module-level variable after line 91 (beside `tool_log_callback`).
- `ai_client._append_comms` (`ai_client.py:136-147`): Add `"source_tier": current_tier` to the entry dict (can be `None` for main-session calls).
- `multi_agent_conductor.run_worker_lifecycle` (`multi_agent_conductor.py:224-354`): Before the `try:` block that calls `ai_client.send()` (line ~296), add `ai_client.current_tier = "Tier 3"`. In the `finally:` block, add `ai_client.current_tier = None`.
- `conductor_tech_lead.generate_tickets` (`conductor_tech_lead.py:6-48`): In the `try:` block before `ai_client.send()`, add `ai_client.current_tier = "Tier 2"`. In `finally:`, add `ai_client.current_tier = None`.
- `gui_2.py _on_tool_log` (`gui_2.py:897-900`): Capture `ai_client.current_tier` at call time and pass it along.
- `gui_2.py _append_tool_log` (`gui_2.py:1496-1503`): Change stored format from `(script, result, time.time())` to dict: `{"script": script, "result": result, "ts": time.time(), "source_tier": source_tier}`.
- Update `_on_tool_log` signature to accept and pass `source_tier`, OR read it directly from `ai_client.current_tier` inside `_append_tool_log`.

### Phase 2 — Tool Log Reader Migration
- `_render_tool_calls_panel` (`gui_2.py:2989-3039`): Replace `script, result, _ = self._tool_log[i_minus_one]` with dict access: `entry = self._tool_log[i_minus_one]`, `script = entry["script"]`, `result = entry["result"]`.
- No filtering yet — just migrate readers so Phase 3 can add filter logic cleanly.

### Phase 3 — Focus Agent UI + Filter Logic
- `App.__init__`: Add `self.ui_focus_agent: str | None = None` after `self.active_tier` (line ~283).
- `_render_tool_calls_panel` AND `_render_comms_history_panel`: At top of each method, derive `log_to_render` by filtering on `entry.get("source_tier") == self.ui_focus_agent` when `ui_focus_agent` is not None.
- **Focus Agent selector widget**: In `_gui_func` Operations Hub block (line ~1774-1783), before the `imgui.begin_tab_bar("OperationsTabs")` call, add:
  ```python
  imgui.text("Focus Agent:")
  imgui.same_line()
  focus_label = self.ui_focus_agent or "All"
  if imgui.begin_combo("##focus_agent", focus_label):
      if imgui.selectable("All", self.ui_focus_agent is None)[0]:
          self.ui_focus_agent = None
      for tier in ["Tier 1", "Tier 2", "Tier 3", "Tier 4"]:
          if imgui.selectable(tier, self.ui_focus_agent == tier)[0]:
              self.ui_focus_agent = tier
      imgui.end_combo()
  ```
- Show `source_tier` label in `_render_comms_history_panel` entry header row (after `provider/model` field).

## Non-Functional Requirements
- `current_tier` must be cleared in `finally` blocks — never left set after a send call.
- Thread safety: `current_tier` is a module-level var. Because `ai_client.send()` calls are serialized (one tier at a time in the MMA engine's executor), race conditions are negligible. Document this assumption in a code comment.
- No new Python package dependencies.
- `_tool_log` dict format change must be handled as a breaking change — confirm no simulation tests directly inspect raw `_tool_log` tuples.

## Architecture Reference
- [docs/guide_architecture.md](../../../docs/guide_architecture.md): Threading model, event system
- [docs/guide_mma.md](../../../docs/guide_mma.md): Worker lifecycle, tier context

## Out of Scope
- Per-tier token stats / token budget panel filtering (separate sub-track).
- Discussion panel role-based tier filtering (the `role` values don't consistently map to tier names; out of scope here).
- Tier 1 (Claude Code conductor) comms — Tier 1 never calls `ai_client.send()`.
- Filtering the Tier 1–4 stream panels (already tier-scoped via `mma_streams` stream_id key).
