|
| 1 | +# 01 — Audit Findings |
| 2 | + |
| 3 | +## Confirmed Issues |
| 4 | + |
| 5 | +### 1. No Streaming — Batch-Only Rendering |
| 6 | + |
| 7 | +**Severity:** Critical |
| 8 | +**Location:** `transcript.tsx`, `sessions/[key]/page.tsx` |
| 9 | + |
| 10 | +The gateway emits `chat` events with `state: "delta"` for streaming chunks and `state: "final"` for completed messages. The UI ignores deltas entirely. The data path is: |
| 11 | + |
| 12 | +1. `sessions/[key]/page.tsx` calls `trpc.sessions.history.queryOptions()` — a standard React Query poll. |
| 13 | +2. React Query uses a 5-second `staleTime` (set in `TRPCReactProvider`). |
| 14 | +3. `sendMutation.onSuccess` triggers `queryClient.invalidateQueries` to refetch history. |
| 15 | + |
| 16 | +**Result:** After the user sends a message, they see nothing until the full response completes and the next poll fires. For long-running agent tasks (tool calls, delegations), this can mean 30+ seconds of dead silence. |
| 17 | + |
| 18 | +**Root cause:** The server receives gateway `chat` events via `gateway.on("event", ...)` in `server.ts`, but only feeds them into the `EventCollector` for the activity stream. No mechanism exists to push delta events to the client. |
| 19 | + |
| 20 | +--- |
| 21 | + |
| 22 | +### 2. Broken Session Metadata Display |
| 23 | + |
| 24 | +**Severity:** High |
| 25 | +**Location:** `session-sidebar.tsx` line 101, `session-workspace.tsx` line 134-135 |
| 26 | + |
| 27 | +**Sidebar:** The title computation `s.label ?? s.derivedTitle ?? s.displayName ?? s.key` works correctly in code, but `label`, `derivedTitle`, and `displayName` are often `undefined` from the gateway. The fallback `s.key` renders raw keys like `agent:main:1771793471876`. |
| 28 | + |
| 29 | +**Header:** The workspace header displays: |
| 30 | +```tsx |
| 31 | +<h2>{session.label ?? session.key}</h2> |
| 32 | +<span className="font-mono">{session.key}</span> |
| 33 | +``` |
| 34 | +When `label` is undefined, the h2 shows the raw key. The monospace key always shows below it — no conditional rendering. |
| 35 | + |
| 36 | +**Root cause:** The gateway's `sessions.list` response only populates `derivedTitle` when `includeDerivedTitles: true` is passed (which it is in `[key]/page.tsx`, but the sidebar data comes from a different query instance). Additionally, new sessions created via `chat.send` don't have a title until the gateway auto-derives one after the first exchange. |
| 37 | + |
| 38 | +--- |
| 39 | + |
| 40 | +### 3. Unhandled Payload Types → Red Error Boundaries |
| 41 | + |
| 42 | +**Severity:** High |
| 43 | +**Location:** `normalize-content.ts`, `transcript.tsx` EventRenderer `default` case |
| 44 | + |
| 45 | +The `parseEvents()` function handles: `tool_use`, `tool_result`, text, `<think>` blocks, `tool_calls`, delegations, and approvals. Any object that doesn't match these patterns falls through to `type: "unknown"`, which renders as: |
| 46 | + |
| 47 | +```tsx |
| 48 | +<div className="bg-red-950/10 border border-red-900/30"> |
| 49 | + <div className="text-red-400 font-semibold">Unrecognized Payload</div> |
| 50 | + <CodeBlock language="json" value={JSON.stringify(event.payload)} /> |
| 51 | +</div> |
| 52 | +``` |
| 53 | + |
| 54 | +**Known unhandled types:** |
| 55 | +- `thinking` blocks sent as `{ type: "thinking", thinking: "..." }` (Claude extended thinking format, distinct from `<think>` tags) |
| 56 | +- `text` blocks sent as `{ type: "text", text: "..." }` within content arrays — these ARE handled, but only when the `text` key is present. If the block has `type: "text"` without a `text` key, it falls through. |
| 57 | +- Status/progress events from tool execution |
| 58 | +- `signature` blocks (`{ type: "signature", ... }`) |
| 59 | + |
| 60 | +--- |
| 61 | + |
| 62 | +### 4. Tag Bleed — Internal Routing Tags in User Feed |
| 63 | + |
| 64 | +**Severity:** Medium |
| 65 | +**Location:** `normalize-content.ts` `parseTextWithThoughts()` |
| 66 | + |
| 67 | +The text parsing function splits on `<think>...</think>` but does not strip other internal tags. The gateway prepends routing tags like `[[reply_to_current]]` or `[[agent:delegated_task]]` to message content for orchestration purposes. These render as visible text in the transcript. |
| 68 | + |
| 69 | +**Fix required:** A tag-stripping pass before rendering text events. Pattern: `[[...]]` at the start of text content. |
| 70 | + |
| 71 | +--- |
| 72 | + |
| 73 | +### 5. Composer Feature Gap Between Pre-Session and In-Session |
| 74 | + |
| 75 | +**Severity:** High |
| 76 | +**Location:** `controls-bar.tsx` vs `sessions/page.tsx` |
| 77 | + |
| 78 | +**Pre-session form (`/sessions`)** offers: |
| 79 | +- Agent selection dropdown |
| 80 | +- Model override (sonnet-4.6, opus-4.6, haiku-4.5, gpt-5.2, gpt-5.3-codex) |
| 81 | +- Workflow template (scaffold, pr-review, atomic-commit) |
| 82 | +- Skill profile (web-tavily, mcp-local, all) |
| 83 | +- File attachments with drag-and-drop |
| 84 | +- 4-level thinking selector (Default, Low, Medium, High) — segmented control |
| 85 | + |
| 86 | +**In-session composer (`/sessions/[key]`)** offers: |
| 87 | +- Textarea |
| 88 | +- "Basic" / "Deep Think" buttons (call `sessions.patch` — different mechanism than send-time params) |
| 89 | +- Abort button |
| 90 | +- Send button |
| 91 | + |
| 92 | +**Missing from in-session:** Model override, workflow, skills, attachments, granular thinking levels, any visual indication of current model/thinking state. |
| 93 | + |
| 94 | +--- |
| 95 | + |
| 96 | +### 6. Additional UX Gaps (Discovered via Code Review) |
| 97 | + |
| 98 | +**No scroll-to-bottom button.** Auto-scroll fires on `messages.length` change (line 106-108 in `transcript.tsx`), but there's no button to re-engage auto-scroll after the user scrolls up. |
| 99 | + |
| 100 | +**No streaming/loading indicator.** When `sendMutation.isPending`, the Send button says "Sending..." but nothing appears in the transcript until the full response arrives. |
| 101 | + |
| 102 | +**No message timestamps by default.** Timestamps exist but are hidden with `opacity-0 group-hover:opacity-100` — invisible unless hovering. |
| 103 | + |
| 104 | +**No copy/action buttons on messages.** No copy-to-clipboard, no message regeneration, no edit-and-resubmit. |
| 105 | + |
| 106 | +**Markdown rendering is minimal.** `renderMessageContent()` only handles code blocks (triple-backtick). Inline code, bold, italic, links, lists, tables, and LaTeX are all rendered as plain text. |
| 107 | + |
| 108 | +**No empty state for Context tab.** The `ActiveContextPanel` shows "No active background operations" with a wrench icon, but gives no guidance on what to expect. |
| 109 | + |
| 110 | +**Right panel is always visible.** The 320px right panel (Context/Config/Orchestration) is hardcoded with no collapse/hide mechanism, reducing transcript width on smaller screens. |
| 111 | + |
| 112 | +--- |
| 113 | + |
| 114 | +## Summary Matrix |
| 115 | + |
| 116 | +| Issue | Severity | Category | Fix Complexity | |
| 117 | +|-------|----------|----------|---------------| |
| 118 | +| No streaming | Critical | Architecture | High — requires event subscription pipeline | |
| 119 | +| Broken metadata | High | Data flow | Medium — query consolidation + title derivation | |
| 120 | +| Unhandled payloads | High | Parsing | Low — extend `parseEvents()` | |
| 121 | +| Tag bleed | Medium | Parsing | Low — regex strip before render | |
| 122 | +| Composer gap | High | Component | Medium — port pre-session controls to in-session | |
| 123 | +| Missing scroll button | Medium | UX | Low | |
| 124 | +| No loading indicator | High | UX | Low-Medium | |
| 125 | +| No message actions | Medium | UX | Medium | |
| 126 | +| Minimal markdown | High | Rendering | Medium — integrate react-markdown | |
| 127 | +| Non-collapsible panel | Medium | Layout | Low | |
0 commit comments