Skip to content

fix(renderer): turn numbering, session origin strip, stable Context pill - #52

Merged
axisrow merged 6 commits into
mainfrom
fix/turns-origin-context
Oct 7, 2026
Merged

axisrow merged 6 commits into
mainfrom
fix/turns-origin-context

Conversation

@axisrow

@axisrow axisrow commented Oct 6, 2026 •

Copy link
Copy Markdown
Owner

Что сломано

Три бага UI, найденные при разборе сессии 61e54b3a (четвёртый — найден при
живой верификации, см. коммит 4):

  1. Нумерация ходов не совпадает с транскриптом. Приложение считало «Turn N»
    по AI-группам, поэтому user-сообщения без ответа номер не потребляли:
    24 user-сообщения → «22 turns», метки сдвинуты.
  2. Непонятно, откуда сессия. Над чатом не было ни проекта, ни директории,
    ни worktree; в сайдбаре — усечённый тег «agent-orc…».
  3. Плашка «Context (N)» постоянно исчезает. При live-обновлении сессии
    conversation получает новые AI-группы без статистики → дефолтная цель memo
    выбирала последнюю AI-группу → ?? [] → размонтирование. Phase 1 полной
    загрузки дополнительно затирала статистику null'ом.

Фиксы (по коммиту, каждый по красному тесту)

1. 41539b0 — нумерация по user-сообщениям

  • Красный тест test/renderer/utils/groupTransformer.test.ts — 5/7 падали:
    [U1, A1, U2, U3, A3] давало [0, 1], ожидание [0, 2] (пустой ход потребил номер).
  • groupTransformer.ts: turnIndex AI-группы = userCount - 1 (ordinal её
    user-сообщения; пустые ходы потребляют номер) в полном и инкрементальном пути.
  • jsonl.ts: sidebar-счётчик turnCount — по user-сообщениям не-sidechain,
    ai-run-логика удалена; парити-тест с числом UserChunk.

2. cc65095 — origin сессии над чатом и в сайдбаре

  • Красные тесты: formatSessionOrigin.test.ts (7 кейсов),
    sessionOriginStrip.test.ts (mount, 2 кейса).
  • Новый formatSessionOrigin.ts: стрип repo · worktree · branch · ~/path
    • сайдбар-тег worktree · branch; SessionOriginStrip между SearchBar и
      ChatHistory; SessionItem рендерит тег, полный путь в тултипе. Вкладки не тронуты.

3. befa49e — плашка Context не мигает

  • Красный describe findLastTrackedAiGroupId в contextTracker.test.ts — 4/4 падали.
  • contextTracker.ts: хелпер «последняя AI-группа со статистикой»;
    ChatHistory memo использует его + сброс, если phase.lastAIGroupId не отслежен.
  • Phase 1 fetchSessionDetail больше не затирает статистику null'ом (per-tab —
    spread предыдущих данных; заменённый таб уже чистится cleanupTabSessionData),
    selectSession явно чистит все три статистических поля при смене сессии.
  • Поведенческое следствие: между live-refresh и следующим полным fetch плашка
    показывает статистику последнего отслеженного хода вместо пустоты.

4. 3ea71cb — компакт-сводки не считаются ходами (найден при живой верификации)

  • На сессии 61e54b3a сайдбар показывал 36 turns, чат — 34 user-хода: две строки
    «This session is being continued…» считались ходами, хотя чат рендерит их как
    CompactBoundary.
  • Красный: turn-count тест со сводкой в фикстуре (5 != 4). Фикс: пропускать
    isCompactSummary в turnCount — правило совпадает с чанк-пайплайном.

5. 956f9f4 — раунд /code-review: верифицированные находки

  • Стрип origin читал session.worktreeName из SessionDetail, но поле заполняется
    только на списках сайдбара — ворктри-сессии показали бы repo · main · branch.
    MiddlePanel теперь деривирует repo/worktree из repositoryGroups
    (resolveSessionOriginGroups, правило то же, что у сайдбар-тегов).
  • Phase 1 сохраняет per-tab статы только той же сессии: selectSession
    переиспользует active tab id для новой сессии — раньше её статы оставались бы
    рядом с чужим conversation до Phase 2.
  • Выбранная фаза с неотслеженной группой рендерит пустую плашку вместо
    инъекций другой фазы.
  • Реюз: formatSessionOriginTooltip (2 ручных join), getBaseName из pathUtils
    вместо локального basename.
  • Известные трейдоффы (в коммит-сообщении): дубли «Turn N» для двух AI-групп
    одного хода (навигация и id-схема ai-N — отдельная задача на group.id),
    стагнация плашки между полными загрузками, turnCount 0 для team-сессий.

Верификация

  • Красный→зелёный по каждому фиксу; полный прогон pnpm test — 929/929 (71 файл),
    typecheck и lint чистые.
  • CI на HEAD (956f9f4): зелёный.
  • Живая проверка в dev-приложении (скриншоты в сессии): сессия 61e54b3a —
    сайдбар «34 turns» = фильтр «User 34» = последний чип «Turn 34» (1:1);
    стрип agent-orchestrator · main · main · ~/Projects/agent-orchestrator над чатом;
    плашка «Context (24)» остаётся на месте через живые обновления сессии.
  • Пропуск: mutation-прогон — TS-тула в проекте нет (mutmut — Python-only);
    компенсировано ассертами на точные значения.
  • Slice-часть фикса 3 стор-тестом не покрыта (потребовал бы весь store) —
    typecheck + живая проверка.

🤖 Generated with Claude Code

axisrow and others added 5 commits October 6, 2026 23:18
Turn N must equal the Nth user message of the transcript, 1:1. The old
numbering counted AI chunks (aiCount), so user messages that produced no
response silently shifted every later label: a 24-user-message session
displayed "22 turns" and Turn 21 pointed at transcript turn 24.

- groupTransformer: both transform paths now derive an AI group's turnIndex
  from the user-chunk counter (userCount - 1, 0-based display convention
  unchanged); compact/system chunks consume no number; AI groups before the
  first user message fall back to AI-sequence numbering.
- jsonl analyzeSessionFileMetadata: turnCount now counts user turns
  (isUserChunkLine semantics, sidechain excluded) instead of closed AI runs.

Red→green: new test/renderer/utils/groupTransformer.test.ts failed 5/7 on
the old numbering ([0,2] expected, [0,1] received for an empty turn), now
7/7; jsonl turn-count and parity tests flipped to user semantics, subsystem
green (171 passed). Known effect: two AI groups of the same turn (post-
compaction continuation) now share a turn number — transcript-true; turn
navigation lands on the first group of that turn.

Co-Authored-By: Claude Code <noreply@anthropic.com>
The chat column showed no indication of which project/directory/worktree
a session belongs to, and the sidebar only showed a truncated worktree
name ("agent-orc…").

- New formatSessionOrigin util: "repo · worktree · branch · ~/short/path"
  strip line and a "worktree · branch" sidebar tag; reuses shortenDisplayPath.
- New SessionOriginStrip component between SearchBar and ChatHistory;
  renders nothing when the origin is unknown.
- MiddlePanel reads the session from per-tab data (tabSessionData[tabId]
  ?? sessionDetail) and repoName from repositoryGroups.
- SessionItem renders the origin tag with the full path as tooltip.

Co-Authored-By: Claude Code <noreply@anthropic.com>
The "Context (N)" pill above the chat kept disappearing:

- During a live refresh the conversation gains new AI groups whose stats
  are not computed until the next full fetch. The pill's memo picked the
  last AI group overall, found no stats for it and rendered nothing.
  Now it targets the last AI group that actually has stats
  (new findLastTrackedAiGroupId helper), and a phase's lastAIGroupId is
  only used when it is tracked.
- Phase 1 of fetchSessionDetail reset sessionContextStats /
  sessionPhaseInfo / sessionClaudeMdStats to null on every fetch, briefly
  unmounting the pill until Phase 2 finished. Phase 1 now leaves existing
  stats in place (per-tab entries spread the previous data; a replaced
  tab's data is already cleaned up by openTab). selectSession still
  clears the stats when switching sessions, now including
  sessionPhaseInfo and sessionClaudeMdStats.

Behavioral note: between a live refresh and the next full fetch the pill
shows the stats of the last tracked turn instead of nothing.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Found during live verification (session 61e54b3a): the sidebar showed
36 turns while the chat showed 34 user turns — the two "This session is
being continued..." compact-summary user lines were counted as turns,
but the chat renders them as CompactBoundary, not user turns.

Red: turn-count test with a compact-summary line in the fixture (5 != 4).
Green: skip isCompactSummary lines, same rule as the chat pipeline.

Co-Authored-By: Claude Code <noreply@anthropic.com>
…harden stats hand-off

Addressed verified findings from the PR #52 code review:

- The origin strip above the chat read session.worktreeName from
  SessionDetail, but that field is only ever tagged onto sidebar LIST
  rows — the detail path never populates it, so non-main worktree
  sessions rendered "repo · main · branch" (wrong worktree) and the
  tooltip lost the worktree part. MiddlePanel now resolves repo/worktree
  from repositoryGroups with the same rule the sidebar tags use
  (resolveSessionOriginGroups; main worktree stays unnamed).
- Both hand-rolled tooltip joins replaced with formatSessionOriginTooltip;
  local basename() replaced with the canonical getBaseName from pathUtils.
- Phase 1 of fetchSessionDetail spreads the previous per-tab data only for
  the same session: selectSession reuses the active tab id for a NEW
  session, so the previous session's context stats could render next to
  the new conversation until Phase 2 replaced them.
- With a context phase explicitly selected but its last AI group untracked
  (live refresh, stats not yet recomputed) the pill now renders empty
  instead of falling back to the last tracked group of another phase.

Not addressed (documented trade-offs / follow-ups): duplicate "Turn N"
numbers for two AI groups of the same user turn (affects the turnIndex
navigation lookup and the injection id scheme "ai-N" — needs a
group-id-based navigation/id redesign), live-refresh pill staleness until
the next full fetch (root-cause fix: schedule Phase-2 recompute in
refreshSessionInPlace), zero turnCount for agent-team sessions whose only
inputs are teammate relays.

Co-Authored-By: Claude Code <noreply@anthropic.com>
…extraction, reset completeness

Addressed verified findings from the second /code-review pass:

- The origin strip no longer synthesizes a "main" worktree segment: it
  duplicated the branch for main-worktree sessions on branch "main"
  ("repo · main · main · ~/path" — the most common session shape) and
  asserted "main" for unresolvable origins in flat sidebar mode. The
  worktree segment renders only when explicitly known.
- Turn-index policy (userCount-1 with AI-sequence fallback) extracted into
  aiTurnIndex — full and incremental transforms must number the same
  transcript identically; the policy now lives in one place with one comment.
- The context pill's target-group resolution moved into contextTracker as
  resolveContextTargetAiGroupId (unit-tested without React: selected phase
  resolves within itself, untracked phase renders empty, no selection picks
  the last tracked group); the memo's dead `else if` condition removed and
  its empty result deduplicated into EMPTY_CONTEXT_RESULT.
- clearSelection and the shared session reset helper now also clear
  sessionPhaseInfo / sessionClaudeMdStats — same incomplete-reset class
  the selectSession hardening fixed.
- jsonl turnCount doc comment, the awaitingAIGroup comment and the
  SessionItem turns tooltip no longer promise "same count as Turn chips"
  for a session whose last turn has no response; tooltip states the rule.
- Parity test hardened: an assistant line after the trailing empty turn
  makes AI-run counting give 4 while user-message counting gives 3 — a
  regression to AI-run semantics now fails the test instead of passing it.

Refuted during verification: switching the scanner's turn predicate to
categorizeMessage would CHANGE semantics — isParsedUserChunkMessage wraps
the canonical isUserChunkLine shared with the turn-budget hook (system-tag
prefixed lines are excluded by both), so scanner and hook stay aligned by
construction. Chunk-factory hoisting declined: the two test factory sets
look alike but differ (seq ids, rawMessages, inline userMessage); the
shape drift it guards against is caught by typecheck.

Known follow-ups unchanged: #53 (turnIndex uniqueness for navigation and
injection ids), #54 (stats recompute in the refresh path; this pass also
showed the pill's token cost freezes with it — noted there).

Co-Authored-By: Claude Code <noreply@anthropic.com>
@axisrow
axisrow merged commit 11b760c into main Oct 7, 2026
1 check passed
@axisrow
axisrow deleted the fix/turns-origin-context branch October 7, 2026 03:06
axisrow added a commit that referenced this pull request Oct 7, 2026
* feat(tracker): use real group.id for injection ids and navigation

Issue #53. After #52 two AI groups of one turn share turnIndex (compact or
system boundaries tear a turn; aiCount fallback collides too), so the
synthetic "ai-${turnIndex}" id collided: duplicate injection ids and
wrong-group navigation.

Trackers now carry the group's real id (= stable chunk id, unique across
re-parses):

- contextTracker: turnGroupId = aiGroup.id; injection id generators take the
  group id instead of turnIndex (tool-output-<id>, loop-<id>, wait-loop-<id>,
  thinking-text-<id>, task-coord-<id>, user-msg-<id>); reread id follows.
- claudeMdTracker: same turnGroupId change; firstSeenTurnIndex (display-only
  "@turn N") added next to firstSeenInGroup.
- ClaudeMdInjection gains firstSeenTurnIndex so labels survive real uuid ids.

Red test (contextTracker.test.ts): two AI groups sharing turnIndex 0 must
keep disjoint injection ids, their own aiGroupId, and claude-md display
metadata — failed with 'ai-0' expected 'ai-xxx'.

Co-Authored-By: Claude Code <noreply@anthropic.com>

* refactor(panel): navigate by group.id instead of turn index

Issue #53, panel half. SessionContextPanel callbacks (onNavigateToTurn /
onNavigateToTool / onNavigateToUserGroup) take the AI group's id instead of
turnIndex; ChatHistory resolves the target by group.id, so a click from the
LATER group of a split turn lands in that group, not the first one sharing
the index.

- Sections/items pass injection.aiGroupId (or firstSeenInGroup for
  claude-md/mentioned-file); clickability gate is a non-empty id.
- FlatInjectionList rows carry groupId; RankedInjectionList swaps
  getInjectionTurnIndex for getInjectionGroupId.
- parseTurnIndex dropped; formatFirstSeen formats a turnIndex number —
  with uuid ids the "ai-N" parse was already unfixable.
- DirectoryTree nodes carry firstSeenTurnIndex for the "@turn N" label.

Burn-header nav tests now assert the callback receives the group id string;
ClaudeMdItem test covers a click with a uuid id (previously not clickable:
parseTurnIndex returned -1).

Co-Authored-By: Claude Code <noreply@anthropic.com>

* chore(test): cap vitest workers at n-1 explicitly

Pin poolOptions.threads (minThreads 1, maxThreads cpus-1) so worker count
does not depend on vitest defaults and the suite cannot saturate the
machine.

Co-Authored-By: Claude Code <noreply@anthropic.com>

* fix(review): harden group-id migration per code-review

Follow-up to the #53 migration from /code-review on PR #56:

- vitest.critical.config.ts: mirror the n-1 worker cap — test:coverage:critical
  would otherwise spawn uncapped workers, the exact load that hung the machine.
- ClaudeMdItem: guard firstSeenTurnIndex with ?? -1 like DirectoryTreeNode —
  an injection without the field rendered "@turn NaN" instead of degrading.
- createGlobalInjections: turnIndex becomes a required parameter (was
  `= 0`); both callers already pass aiGroup.turnIndex, the default silently
  mislabeled a future caller as "@turn 1".
- Stale doc examples in contextInjection.ts: ids are now
  "<prefix>-<group id>" / real group ids, not "ai-0".
- burnHeaderNav fixtures: injection ids follow the real scheme
  ("wait-loop-<group id>"), so an id-scheme regression can't keep them green.
- formatFirstSeen moved into utils/formatting.ts; pathParsing.ts deleted —
  it parsed no paths after parseTurnIndex's removal.

Co-Authored-By: Claude Code <noreply@anthropic.com>

---------

Co-authored-by: axisrow <axisrow@users.noreply.github.com>
Co-authored-by: Claude Code <noreply@anthropic.com>
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