Skip to content

[codex] Fix live arena SSE and add commentary booth#384

Merged
Atharva-Kanherkar merged 4 commits into
mainfrom
codex/live-view-commentary-sidebar
Apr 22, 2026
Merged

[codex] Fix live arena SSE and add commentary booth#384
Atharva-Kanherkar merged 4 commits into
mainfrom
codex/live-view-commentary-sidebar

Conversation

@Atharva-Kanherkar
Copy link
Copy Markdown
Collaborator

What changed

  • normalize incoming run SSE envelopes in the web client so the live arena consumes the backend's snake_case event wire format
  • add focused tests for SSE normalization and the run-detail live event path
  • add an optional commentary booth sidebar on the run detail page, enabled with a toggle and fed from the same live event stream
  • add commentary helpers and UI for bounded, deduplicated play-by-play updates without any challenge-pack configuration changes

Why

The live arena could show the run as Live while each lane stayed stuck because the frontend was reading EventID/RunAgentID/EventType, but the backend SSE stream is emitted as event_id/run_agent_id/event_type. As a result, events parsed but the live lane reducer saw empty fields and never advanced lane state.

User impact

  • live agent lanes now update correctly during active runs
  • users can opt into a sidebar commentator feed for quick play-by-play context
  • no backend contract or challenge-pack config changes are required

Validation

  • cd web && npm test -- src/hooks/use-run-events.test.ts src/hooks/use-agent-commentary.test.ts 'src/app/(workspace)/workspaces/[workspaceId]/runs/[runId]/run-detail-client.test.tsx'
  • cd web && npm run lint -- src/hooks/use-run-events.ts src/hooks/use-run-events.test.ts src/hooks/use-agent-commentary.ts src/hooks/use-agent-commentary.test.ts src/components/arena/live-commentary-sidebar.tsx 'src/app/(workspace)/workspaces/[workspaceId]/runs/[runId]/run-detail-client.tsx' 'src/app/(workspace)/workspaces/[workspaceId]/runs/[runId]/run-detail-client.test.tsx'

Notes

  • manual browser smoke testing was not run in this pass

Checkpoint: completes reviewed steps 1-2 from testing/codex-live-view-commentary-sidebar.md.
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 22, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agentclash Ready Ready Preview, Comment Apr 22, 2026 11:33am

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 22, 2026

Greptile Summary

This PR fixes a critical SSE field-name mismatch that caused live arena lanes to remain stuck during active runs, and adds an optional commentary sidebar as a new feature.

Core bug fix — SSE normalization:

  • normalizeRunEvent in use-run-events.ts maps the backend's snake_case wire format (event_id, run_agent_id, event_type, etc.) to the Pascal/camelCase RunEvent shape the live arena reducer expects. Without this, all dispatched events had empty EventType and RunAgentID fields and lane state never advanced.
  • The function is idempotent — already-normalized objects pass through correctly, so no existing callers regress.

New commentary sidebar:

  • useAgentCommentary is a bounded (MAX_COMMENTARY_ENTRIES = 24), deduplicated useReducer-based hook that derives play-by-play text from the same SSE event stream.
  • LiveCommentarySidebar renders the most recent entries in a sticky column alongside the agent lanes, toggled by a new header button.
  • Commentary state accumulates continuously regardless of sidebar visibility; three P2 items to consider:
    • reset() is exported but never called — stale entries from before the toggle are shown when re-enabling the sidebar.
    • slice(-12) in the sidebar displays only half of the 24-entry store; these limits should align or be named consistently.
    • formatClock uses the browser's local timezone for UTC backend timestamps.

Test coverage:

  • Unit tests for normalizeRunEvent (both snake_case and already-normalized paths), commentary helper suppression/generation, deduplication, and bounding.
  • Integration test confirming the toggle → SSE event → lane + sidebar update pipeline end-to-end.

Confidence Score: 4/5

Safe to merge — the core bug fix is correct and well-tested; remaining concerns are all non-blocking P2 style items.

The SSE normalization fix is the primary goal and is implemented correctly with solid unit and integration test coverage. The commentary feature is well-structured (bounded store, deduplication, correct reducer pattern, proper cleanup effect). All open issues are P2: a display-limit vs store-limit mismatch, unused reset() leading to potentially stale sidebar history on re-enable, and local-timezone clock formatting. None of these block production use.

live-commentary-sidebar.tsx (slice vs MAX mismatch, local-timezone clock) and run-detail-client.tsx (unused reset() on toggle-off).

Important Files Changed

Filename Overview
web/src/hooks/use-run-events.ts Adds normalizeRunEvent to map snake_case SSE wire format to the Pascal/camelCase RunEvent shape; hooks into the SSE listener correctly with a stable onEventRef pattern.
web/src/hooks/use-agent-commentary.ts New hook with bounded, deduplicated commentary reducer; well-structured, but reset() is exported and never used, leading to stale entries accumulating while the sidebar is hidden.
web/src/components/arena/live-commentary-sidebar.tsx New sidebar component; slice(-12) display limit mismatches MAX_COMMENTARY_ENTRIES = 24, and formatClock uses local timezone instead of UTC for backend ISO timestamps.
web/src/app/(workspace)/workspaces/[workspaceId]/runs/[runId]/run-detail-client.tsx Wires commentary hook into the SSE event pipeline and adds the toggle + sidebar layout; adds correct cleanup useEffect for fetchTimerRef; reset() is not called on toggle-off.
web/src/hooks/use-run-events.test.ts Good coverage of normalizeRunEvent for both snake_case and already-normalized inputs, plus an integration test for the SSE listener normalization path.
web/src/hooks/use-agent-commentary.test.ts Tests deduplication, bounding, event suppression, and meaningful event commentary generation; well-structured.
web/src/app/(workspace)/workspaces/[workspaceId]/runs/[runId]/run-detail-client.test.tsx Integration test for the toggle + live event → lane + sidebar update pipeline; good use of real module parts via vi.importActual.

Sequence Diagram

sequenceDiagram
    participant BE as Backend SSE
    participant UE as useRunEvents
    participant NE as normalizeRunEvent
    participant HS as handleSSEEvent
    participant UA as useAgentArena
    participant UC as useAgentCommentary
    participant UI as RunDetailClient

    BE->>UE: run_event (snake_case JSON)
    UE->>NE: normalizeRunEvent(raw)
    NE-->>UE: RunEvent (PascalCase fields)
    UE->>HS: onEvent(RunEvent)
    HS->>UA: handleArenaEvent(event)
    UA-->>UI: updated arenaLanes → LiveAgentLane re-renders
    HS->>UC: handleCommentaryEvent(event)
    UC-->>UI: updated entries → LiveCommentarySidebar (if showCommentary)
    HS->>HS: debounce 300ms → fetchAll()
Loading

Comments Outside Diff (1)

  1. web/src/app/(workspace)/workspaces/[workspaceId]/runs/[runId]/run-detail-client.tsx, line 310-313 (link)

    P2 Commentary entries accumulate while the sidebar is hidden; reset is never called

    handleCommentaryEvent is dispatched on every SSE event regardless of the showCommentary toggle. When a user turns commentary off and later turns it back on, they see up to 24 stale entries from before they re-enabled the sidebar. The reset() function exported from useAgentCommentary is never called anywhere.

    If the intended UX is a fresh feed each time the user opts in, call reset() when the toggle transitions to false:

    onClick={() =>
      setShowCommentary((current) => {
        if (current) reset();
        return !current;
      })
    }

    If showing history-on-re-enable is intentional, a brief code comment explaining that design choice would help future maintainers.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: web/src/app/(workspace)/workspaces/[workspaceId]/runs/[runId]/run-detail-client.tsx
    Line: 310-313
    
    Comment:
    **Commentary entries accumulate while the sidebar is hidden; `reset` is never called**
    
    `handleCommentaryEvent` is dispatched on every SSE event regardless of the `showCommentary` toggle. When a user turns commentary off and later turns it back on, they see up to 24 stale entries from before they re-enabled the sidebar. The `reset()` function exported from `useAgentCommentary` is never called anywhere.
    
    If the intended UX is a fresh feed each time the user opts in, call `reset()` when the toggle transitions to `false`:
    
    ```ts
    onClick={() =>
      setShowCommentary((current) => {
        if (current) reset();
        return !current;
      })
    }
    ```
    
    If showing history-on-re-enable is intentional, a brief code comment explaining that design choice would help future maintainers.
    
    How can I resolve this? If you propose a fix, please make it concise.

    Fix in Codex

Fix All in Codex

Prompt To Fix All With AI
This is a comment left during a code review.
Path: web/src/components/arena/live-commentary-sidebar.tsx
Line: 44

Comment:
**Display limit doesn't match bounded store size**

The sidebar renders only the last 12 entries via `entries.slice(-12)`, but `MAX_COMMENTARY_ENTRIES` is 24. This means `useAgentCommentary` stores up to 24 entries in memory, but only half are ever visible in the sidebar. The scrollable `max-h-[34rem]` container with `space-y-2` has room for more entries. Either the display slice should match `MAX_COMMENTARY_ENTRIES` (or a named constant derived from it), or the store limit should be set to 12 to avoid the mismatch.

```suggestion
  const recent = entries.slice(-MAX_COMMENTARY_ENTRIES);
```

(Requires importing `MAX_COMMENTARY_ENTRIES` from `@/hooks/use-agent-commentary`.)

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: web/src/components/arena/live-commentary-sidebar.tsx
Line: 21-27

Comment:
**`formatClock` renders timestamps in the user's local timezone**

`d.getHours()` / `d.getMinutes()` / `d.getSeconds()` use the browser's local timezone. The `occurredAt` timestamps from the backend are UTC ISO-8601 strings. Users in non-UTC timezones will see clock values that are offset from the actual event time, and there is no timezone indicator displayed.

Consider using UTC accessors (`getUTCHours`, `getUTCMinutes`, `getUTCSeconds`) plus a "UTC" label, or `Intl.DateTimeFormat` with `timeZone: "UTC"` (or the user's explicit preference):

```suggestion
function formatClock(iso: string): string {
  const d = new Date(iso);
  const h = d.getUTCHours().toString().padStart(2, "0");
  const m = d.getUTCMinutes().toString().padStart(2, "0");
  const s = d.getUTCSeconds().toString().padStart(2, "0");
  return `${h}:${m}:${s}`;
}
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: web/src/app/(workspace)/workspaces/[workspaceId]/runs/[runId]/run-detail-client.tsx
Line: 310-313

Comment:
**Commentary entries accumulate while the sidebar is hidden; `reset` is never called**

`handleCommentaryEvent` is dispatched on every SSE event regardless of the `showCommentary` toggle. When a user turns commentary off and later turns it back on, they see up to 24 stale entries from before they re-enabled the sidebar. The `reset()` function exported from `useAgentCommentary` is never called anywhere.

If the intended UX is a fresh feed each time the user opts in, call `reset()` when the toggle transitions to `false`:

```ts
onClick={() =>
  setShowCommentary((current) => {
    if (current) reset();
    return !current;
  })
}
```

If showing history-on-re-enable is intentional, a brief code comment explaining that design choice would help future maintainers.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "feat(web): fix live arena SSE and add co..." | Re-trigger Greptile

className,
}: LiveCommentarySidebarProps) {
const scrollRef = useRef<HTMLDivElement>(null);
const recent = entries.slice(-12);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Display limit doesn't match bounded store size

The sidebar renders only the last 12 entries via entries.slice(-12), but MAX_COMMENTARY_ENTRIES is 24. This means useAgentCommentary stores up to 24 entries in memory, but only half are ever visible in the sidebar. The scrollable max-h-[34rem] container with space-y-2 has room for more entries. Either the display slice should match MAX_COMMENTARY_ENTRIES (or a named constant derived from it), or the store limit should be set to 12 to avoid the mismatch.

Suggested change
const recent = entries.slice(-12);
const recent = entries.slice(-MAX_COMMENTARY_ENTRIES);

(Requires importing MAX_COMMENTARY_ENTRIES from @/hooks/use-agent-commentary.)

Prompt To Fix With AI
This is a comment left during a code review.
Path: web/src/components/arena/live-commentary-sidebar.tsx
Line: 44

Comment:
**Display limit doesn't match bounded store size**

The sidebar renders only the last 12 entries via `entries.slice(-12)`, but `MAX_COMMENTARY_ENTRIES` is 24. This means `useAgentCommentary` stores up to 24 entries in memory, but only half are ever visible in the sidebar. The scrollable `max-h-[34rem]` container with `space-y-2` has room for more entries. Either the display slice should match `MAX_COMMENTARY_ENTRIES` (or a named constant derived from it), or the store limit should be set to 12 to avoid the mismatch.

```suggestion
  const recent = entries.slice(-MAX_COMMENTARY_ENTRIES);
```

(Requires importing `MAX_COMMENTARY_ENTRIES` from `@/hooks/use-agent-commentary`.)

How can I resolve this? If you propose a fix, please make it concise.

Fix in Codex

Comment on lines +21 to +27
function formatClock(iso: string): string {
const d = new Date(iso);
const h = d.getHours().toString().padStart(2, "0");
const m = d.getMinutes().toString().padStart(2, "0");
const s = d.getSeconds().toString().padStart(2, "0");
return `${h}:${m}:${s}`;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 formatClock renders timestamps in the user's local timezone

d.getHours() / d.getMinutes() / d.getSeconds() use the browser's local timezone. The occurredAt timestamps from the backend are UTC ISO-8601 strings. Users in non-UTC timezones will see clock values that are offset from the actual event time, and there is no timezone indicator displayed.

Consider using UTC accessors (getUTCHours, getUTCMinutes, getUTCSeconds) plus a "UTC" label, or Intl.DateTimeFormat with timeZone: "UTC" (or the user's explicit preference):

Suggested change
function formatClock(iso: string): string {
const d = new Date(iso);
const h = d.getHours().toString().padStart(2, "0");
const m = d.getMinutes().toString().padStart(2, "0");
const s = d.getSeconds().toString().padStart(2, "0");
return `${h}:${m}:${s}`;
}
function formatClock(iso: string): string {
const d = new Date(iso);
const h = d.getUTCHours().toString().padStart(2, "0");
const m = d.getUTCMinutes().toString().padStart(2, "0");
const s = d.getUTCSeconds().toString().padStart(2, "0");
return `${h}:${m}:${s}`;
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: web/src/components/arena/live-commentary-sidebar.tsx
Line: 21-27

Comment:
**`formatClock` renders timestamps in the user's local timezone**

`d.getHours()` / `d.getMinutes()` / `d.getSeconds()` use the browser's local timezone. The `occurredAt` timestamps from the backend are UTC ISO-8601 strings. Users in non-UTC timezones will see clock values that are offset from the actual event time, and there is no timezone indicator displayed.

Consider using UTC accessors (`getUTCHours`, `getUTCMinutes`, `getUTCSeconds`) plus a "UTC" label, or `Intl.DateTimeFormat` with `timeZone: "UTC"` (or the user's explicit preference):

```suggestion
function formatClock(iso: string): string {
  const d = new Date(iso);
  const h = d.getUTCHours().toString().padStart(2, "0");
  const m = d.getUTCMinutes().toString().padStart(2, "0");
  const s = d.getUTCSeconds().toString().padStart(2, "0");
  return `${h}:${m}:${s}`;
}
```

How can I resolve this? If you propose a fix, please make it concise.

Fix in Codex

Checkpoint: completes reviewed step 3 from testing/codex-live-view-commentary-sidebar.md.
@Atharva-Kanherkar Atharva-Kanherkar merged commit 4b82f44 into main Apr 22, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant