Skip to content

CPLAT-10756: fix refs preview hang + fast render, dedup, timestamps#58

Merged
gavin-jeong merged 5 commits into
masterfrom
CPLAT-10756-refs-resolve-hang
Jul 8, 2026
Merged

CPLAT-10756: fix refs preview hang + fast render, dedup, timestamps#58
gavin-jeong merged 5 commits into
masterfrom
CPLAT-10756-refs-resolve-hang

Conversation

@gavin-jeong

@gavin-jeong gavin-jeong commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

JIRA: https://sendbird.atlassian.net/browse/CPLAT-10756

References 프리뷰 hang 수정 + 렌더 속도/유용성 개선. 두 커밋으로 구성.

1. Hang 수정 (fix: resolve refs preview hang)

프리뷰가 **"Resolving PR/Jira status…"**에서 무한 대기하던 버그.

  • resolve cmd 유실: render 경로(renderSessionSplit)가 async cmd를 _ =로 버림 → liveUpdate off면 영영 안 됨. → pending 타깃을 App에 기록 후 handleTick 최상단(liveUpdate 게이트 이전)에서 flush.
  • compare URL만 있는 세션: 빈 Refs + RefsResolved=true인데 "Resolving…" 표시. → renderSessionRefsresolved 인자 추가, "No resolvable PR/Jira references." 표시.

2. 빠른 렌더 + dedup + timestamp (perf: render refs instantly…)

  • extract/resolve 분리: 프리뷰 열 때 오프라인 extract(파싱→URL/label)를 먼저 하고 즉시 렌더(refsExtractedMsg), 상태는 2단계로 async resolve(refsEnrichedMsg). 대형 transcript도 gh/Jira 왕복을 기다리지 않음.
  • Label 기준 dedup: 같은 PR의 다른 URL 형태(#discussion, 쿼리스트링)를 한 행으로 합침 → 중복 PR link 문제 해결.
  • FirstSeen timestamp: ref 최초 등장 시점을 기록(dedup 시 가장 이른 값 유지), 종류별 최신순 정렬 + 각 행에 "· N ago" 표시.
  • URL 키 상태 캐시에서 FirstSeen/Label 보존.

Tests

  • TestExtractSessionRefsDedupAndTimestamp (dedup + FirstSeen + 최신순)
  • TestRenderSessionRefs (resolved-empty 케이스)
  • session + tui 스위트 통과

Security checklist (Infrastructure code)

  • I have checked below conditions:
    • No SecurityGroup rule changes
    • No public open SecurityGroup inbound rules
    • No resources in public subnet
    • No credentials committed
    • No IAM users added
    • Follows OWASP / SendBird SSDLC guidance

JIRA: https://sendbird.atlassian.net/browse/CPLAT-10756

The References preview could get stuck on "Resolving PR/Jira status…"
forever. Two causes:

1. updateSessionRefsPreview returns an async resolve tea.Cmd, but the
   render path (View → renderSessionSplit) calls it with `_ =`, discarding
   the cmd. With liveUpdate off, no navigation followed to re-issue it, so
   the resolve never ran. Fix: record the pending resolve target on the App
   and flush it to a dispatched cmd at the top of handleTick (before the
   liveUpdate gate), so it always runs on the next 3s tick regardless of
   entry path or liveUpdate.

2. When a session's only links were unresolvable (e.g. /pull/new compare
   URLs, now filtered out), refs stayed empty with RefsResolved=true, and
   renderSessionRefs still showed "Resolving…". Add a `resolved` arg so an
   empty-but-resolved list renders "No resolvable PR/Jira references."

Also re-render the open refs preview on refsEnrichedMsg even when no refs
landed, so the placeholder flips instead of spinning.
@Kairo-Kim Kairo-Kim added the auto-review/approved Auto-approved by the Slack auto-reviewer bot label Jul 7, 2026
…amps

JIRA: https://sendbird.atlassian.net/browse/CPLAT-10756

Speed up the References preview and make its list more useful:

- Split the offline extract (transcript parse → URLs/labels) from the
  network-bound status resolve. On preview open we now extract first
  (refsExtractedMsg) and render URLs/labels/timestamps immediately, then
  resolve status in a second step (refsEnrichedMsg). Large transcripts no
  longer block the whole preview on gh/Jira round-trips.
- Dedup references by canonical Label instead of raw URL, so the same PR
  referenced via different URL forms (#discussion anchors, query strings)
  collapses to one row — fixes the duplicate-PR-link problem.
- Record FirstSeen (the timestamp of the entry where a ref first appeared)
  and keep the earliest on dedup. Sort most-recent-first within each kind
  and show a "· <time> ago" suffix per row.
- Preserve FirstSeen/Label across the URL-keyed status cache.
@gavin-jeong gavin-jeong changed the title CPLAT-10756: fix refs preview stuck on Resolving… CPLAT-10756: fix refs preview hang + fast render, dedup, timestamps Jul 7, 2026

@jinsekim jinsekim left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

LGTM!

@jinsekim jinsekim left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

LGTM!

JIRA: https://sendbird.atlassian.net/browse/CPLAT-10756

Status resolution was one sequential pass over all refs returning a single
batched message, so the preview stayed on "Resolving…" until the slowest
ref (a timing-out Jira call, or the last of ~20 gh subprocesses) finished.

- Resolve each ref in its own tea.Cmd and stream results back individually
  (refStatusMsg). Refs are dispatched in display order (most-recent first),
  so the newest PR/Jira status fills in within ~0.5s while the rest arrive
  as they land, instead of all-or-nothing after ~12s. Extracted +
  ResolveRef(single) added to the session package; ResolveRefs now delegates.
- Merge each streamed status into the session by URL and re-render the open
  refs preview live; mark RefsResolved only once every ref has a status.
- Circuit-break Jira auth: a 401/403 means the token is unusable for every
  issue, so trip a process-wide breaker and skip further Jira round-trips
  (httpDoJSON now surfaces the HTTP status for this).

Measured on a 13MB transcript: extract 149ms (list shows instantly), each
PR resolves in ~0.5-0.7s and streams in independently.

@jinsekim jinsekim left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

LGTM!

@jinsekim jinsekim left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

LGTM!

JIRA: https://sendbird.atlassian.net/browse/CPLAT-10756

Per-ref streaming resolve, combined with the tick-driven background
enrich, could fan out unbounded gh subprocesses and re-parse large
transcripts every 3s tick — CPU spiked and the session list stopped
responding while "Resolving…" was up. Root causes and fixes:

- No cap on concurrent network resolves. A preview with N refs dispatched
  N gh subprocesses at once (plus the background sweep). Add a process-wide
  semaphore (4) in ResolveRef around the gh/Jira call; cache hits bypass it.
  Re-check the cache after acquiring a slot so refs that resolved while we
  waited don't re-run gh.
- Duplicate resolve passes. enrichRefsCmd re-targeted the same sessions
  every tick because RefsResolved stays false during the (slow) pass, and a
  live session's mtime change reset RefsResolved mid-pass. Track in-flight
  session IDs (refsInFlight) and skip re-targeting / mtime-reset while a
  pass is running; clear on completion. This stops the repeated 13MB
  transcript re-parses that dominated CPU.

Net effect: bounded gh fan-out, no redundant re-parsing, UI stays
responsive during resolution.
JIRA: https://sendbird.atlassian.net/browse/CPLAT-10756

Finding PR/Jira links should be trivial, but ExtractSessionRefsFromFile
went through LoadMessages → ParseEntry, fully JSON-unmarshaling every entry
(hooks, tool blocks, content) of a multi-MB transcript just to read a few
URLs. Measured 149ms on a 13MB session — the real cost behind the slow
"Resolving…", not the network.

Rewrite the file extractor to scan raw lines: run the URL regex over each
line's bytes and, only for lines that contain a ref, pull the timestamp out
with a cheap string scan (no JSON decode). Same dedup-by-label, same
FirstSeen/ordering. Measured 10ms on the same 13MB file (~15x faster).

The entry-based ExtractSessionRefs is kept for callers that already hold
parsed entries. Adds TestExtractSessionRefsFromFile covering the raw-line
path (URL parse, per-line timestamp, dedup, ordering).

@jinsekim jinsekim left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

LGTM!

@jinsekim jinsekim left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

LGTM!

@gavin-jeong gavin-jeong merged commit 5799cd9 into master Jul 8, 2026
3 checks passed
@gavin-jeong gavin-jeong deleted the CPLAT-10756-refs-resolve-hang branch July 8, 2026 00:07
gavin-jeong added a commit that referenced this pull request Jul 9, 2026
* fix: dispatch ref extract on entry, keep View pure (fix stuck/lag/CPU)

JIRA: https://sendbird.atlassian.net/browse/CPLAT-10756

The References preview still had three symptoms — stuck on "Resolving…",
lag, and CPU spikes. Root cause was structural: the extract/resolve was
never dispatched from a real Update path, so it leaned on the View render
path (which discards cmds) plus a tick flush.

- setSessPreviewMode returned no cmd. Entering References mode therefore
  dispatched nothing; the extract only ran when the View render path
  happened to call updateSessionRefsPreview (cmd discarded) and handleTick
  later flushed a pending slot. That added up-to-3s latency and, if
  navigation intervened, left the pane stuck forever. Fixed: setSessPreviewMode
  now returns updateSessionPreview()'s cmd, and every caller dispatches it.
- The View render path (renderSessionSplit) called updateSessionPreview for
  refs mode every frame, mutating refsInFlight as a side effect while
  discarding the returned extract cmd — the exact shape that stranded a
  session on "Resolving…". Fixed: exclude refs (like live) from the View
  path; it is initialized from Update paths only, and re-rendered on resize
  via resizeAll (which can dispatch).
- updateSessionRefsPreview allocated a fresh viewport.New on every call.
  Fixed: only reallocate when the pane dimensions actually change.

Removes the sessRefsPending map + handleTick flush crutch (no longer needed
now that entry/navigation/resize all dispatch the cmd directly). Replaces
the pending-flush test with TestSetRefsPreviewModeDispatchesExtract, which
asserts entering refs mode returns a cmd that extracts the selected session.

* fix: read refs from a.sessions, not stale list copy (real Resolving… root cause)

JIRA: https://sendbird.atlassian.net/browse/CPLAT-10756

The References preview kept getting stuck on "Resolving…" no matter where the
extract/resolve was dispatched from. Five prior fixes moved the dispatch around
(#57/#58/#60/#61 + the on-entry commit) because they read the symptom as a
missing dispatch. The dispatch was fine — a.sessions WAS being updated. The
render path was reading the wrong copy.

Root cause is duplicated state with the reader pointed at the stale half:

- refsExtractedMsg / refStatusMsg update a.sessions[i].Refs (the source of
  truth) but do NOT rebuild the session list.
- updateSessionRefsPreview renders from selectedSession(), which returns the
  list widget's sessionItem.sess — a copy snapshotted at the last
  rebuildSessionList. That copy's Refs/RefsResolved never update, so the
  preview sees len(refs)==0 && HasRefs && !RefsResolved forever → "Resolving…",
  while refsInFlight stays armed and blocks re-extract.

Every other preview mode (memory/tasks/workflows) stores results in an
App-level cache keyed by session ID, so they were immune; refs alone stored
state inside the session slice.

Fixes:
- updateSessionRefsPreview now re-reads the authoritative session from
  a.sessions (new sessionByIDFromStore) instead of trusting the passed-in list
  copy. Covers both the async-handler and navigation paths.
- carryOverRefState preserves resolved/mid-flight Refs across a full rescan
  (manual refresh, new-session autorefresh), which replaced a.sessions with a
  freshly-scanned slice that has HasRefs but empty Refs — blanking resolved
  refs and, mid-resolve, stranding on a bogus "No resolvable references".
  Transcript-grew sessions keep cached refs but clear RefsResolved for one
  re-resolve; in-flight sessions are left for their pass to finish.

Tests: TestRefsExtractedReflectedInPreview (fails before the read-from-store
fix), TestRefsSurviveRescan, TestCarryOverRefStateReresolvesOnTranscriptGrowth,
TestCarryOverRefStateKeepsMidFlight.

* feat: fill open-PR/Jira badges asynchronously for on-screen sessions

JIRA: https://sendbird.atlassian.net/browse/CPLAT-10756

Previously the open-PR/Jira badge only appeared after the user opened a
session's References preview, because ref status was resolved on demand for the
open preview only. Sessions you never previewed showed no badge even when they
had open PRs.

Now badges fill in asynchronously for the sessions currently ON SCREEN:

- resolveVisibleRefsCmd extracts refs for the visible page slice
  (Paginator.GetSliceBounds) whose links aren't resolved yet, dispatched from
  handleTick and the navigation debounce. refsInFlight dedups so a row visible
  across many ticks is worked once.
- syncSessionRefsToList copies resolved state from a.sessions (source of truth)
  into the list-widget row copy so OpenRefCounts — and thus the badge — updates
  live, without a full rebuildSessionList that would reset scroll/cursor.
- refStatusMsg now calls syncSessionRefsToList so the badge fills in whether or
  not the References preview is open.

Deliberately scoped to the visible page, NOT the fleet. A tick-driven sweep
across every HasRefs session is exactly what pegged CPU for minutes (each
`gh pr view` ~1.6s, hundreds of them) and was removed in #60. Bounding work to
what the user can see keeps the fan-out tiny: extract is a ~10ms offline scan
and the follow-on status resolve is already capped at 4 concurrent.

Tests: TestResolveVisibleRefsCmdOnlyVisibleUnresolved (scope + dedup),
TestSyncSessionRefsToListUpdatesBadge (badge reflects resolved state).

* fix: resolve refs stuck on "Resolving…" in projectCentric mode + phase-2 wipe

JIRA: https://sendbird.atlassian.net/browse/CPLAT-10756

The References preview kept sticking on "Resolving PR/Jira status…" with no
open-PR/Jira badge for the current-window live session, despite the earlier
read-from-store and carry-over fixes. Two more root causes, both reproduced by
new tests before fixing:

1. projectCentric group mode (the DEFAULT) never dispatched the extract.
   In projectCentric mode a project's session renders as a projectItem head
   row, not a sessionItem. updateSessionPreview took the selectedProject()
   branch, called updateProjectPreview (project summary — no refs), and
   returned nil. updateSessionRefsPreview was never reached, so the extract
   cmd was never dispatched and the preview stuck on "Resolving…" forever.
   The prior fixes all exercised the flat-mode sessionItem path and missed
   this. Fix: in refs mode, route a projectItem through the session refs path
   using its representative session (pi.sessions[0]).

2. phase-2 full scan wiped refs extracted during phase-1.
   sessionsScannedMsg replaced a.sessions with a freshly-scanned slice
   (HasRefs=true, empty Refs) without carrying over ref state, blanking refs
   the user already extracted and reverting to "Resolving…". Fix: call
   carryOverRefState before the swap (same treatment doRefresh already had).

Also hardened the refsInFlight dedup guard: it was armed before checking
whether extractSessionRefsCmd actually returned a cmd. A session with no
FilePath yields nil, so the guard latched with nothing to clear it — a
permanent "Resolving…" with no retry. Now armed only when a cmd is dispatched,
in both updateSessionRefsPreview and resolveVisibleRefsCmd.

And extended syncSessionRefsToList to update projectItem rows (re-summing
openPRs) so the open-PR badge fills in on the project head row too.

Tests (refs_stuck_test.go): projectCentric dispatch + badge, phase-2 survival,
live full-flow, refsInFlight leak guard.

* fix: dispatch ref extract when refs preview mode is restored at startup

JIRA: https://sendbird.atlassian.net/browse/CPLAT-10756

This is the actual root cause behind the persistent "Resolving PR/Jira status…"
the user kept hitting, which the earlier projectCentric/phase-2/leak fixes did
not touch: the "refs" preview mode is PERSISTED across runs (preview_mode: refs
in prefs).

On startup NewApp restores sessPreviewMode=refs + sessSplit.Show=true directly,
NOT through setSessPreviewMode — the only path that dispatches the offline
extract. The first WindowSizeMsg then hits resizeAll's startup branch
(sessionList.Width()==0), which dispatched the restored preview's async cmd for
LIVE mode only. Refs mode fell through, so the extract was never dispatched:
View() deliberately skips refs mode (it can't dispatch a cmd), and no tick path
re-initializes a preview that was never opened via setSessPreviewMode. Result:
refs stayed empty and the pane sat on "Resolving…" indefinitely.

Fix: in the startup-restore branch, dispatch updateSessionPreview for refs mode
too (not just live). Reproduced by refs_startup_test.go in both flat and the
default projectCentric group modes before fixing.

Also make newTestApp hermetic: a persisted "refs" mode now arms refsInFlight at
startup, so clear refsInFlight + sessSplit.Show in the test harness, otherwise
ref tests inherit in-flight state from the developer's local prefs and see a
nil extract cmd.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

auto-review/approved Auto-approved by the Slack auto-reviewer bot

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants