Repository navigation
fix(chat-history): hide worked-for bar on turns with no agent output - #1802
Merged
Merged
Conversation
Older turns load as placeholders whose bodyEventCount decides whether the chat draws an "Agent worked for X" bar. Every producer over-counted turns the agent never answered, so an interrupted prompt showed a bar over nothing and split the user messages around it. Count only what the chat renders: - Claude Code index: skip attachments, snapshots, queue rows and system bookkeeping rows, and drop the minimum-of-one floor. - Codex catalog: count only rollout lines the parser renders, drop the floor, and keep the measured count on the previous-turn placeholder. - Cursor summaries: count only bubbles that render (cache tag v3). - Native turn index: lifecycle rows are activity, not body (index v14). - Shared projector: skip blank replies and thoughts. In the chat, a historical turn without body gets no bar and no blank placeholder row, and the next user message stacks under it. The newest turn keeps its bar.
FresnelR27
marked this pull request as ready for review
September 15, 2026 08:05
Merge develop while preserving the empty-turn spacing regression tests and the streaming completion tests. Cover the lifecycle-only history case with both collapsed and expanded defaults. Verification: 550 chat-history tests passed; 108 Codex tests passed (3 ignored); whole-project tsgo and targeted ESLint passed.
Initialize the shared-session fixture with the production connection settings before creating its schema. Otherwise the two child processes can race to change the journal mode and fail with SQLITE_BUSY before testing concurrent event persistence. Keep concurrent writes and read-back/isolation assertions unchanged. A focused SQLite reproduction failed 62/100 times with the original setup and 0/100 with WAL initialized first; rustfmt and diff checks passed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
In session history, a turn the agent never answered (for example a prompt interrupted right away) still shows an "Agent worked for X" bar with nothing under it, and the user messages around it are split into separate rounds with full turn spacing.
Root cause: older turns load as
unloadedTurnplaceholders, and the chat draws the bar whenbodyEventCount >= 1. Every producer of that count over-counted rounds that render nothing:total_tokens_reminderattachment,file-history-snapshot,queue-operation, API-error retries) and floored the count at 1, so an interrupted prompt always advertised a body.turn_context,token_count, lifecycle rows, user-message mirrors), also floored at 1. Loading a Codex turn additionally rebuilt the previous turn's placeholder with a count of 0, which replaced the measured one on merge.task_start/task_failed), which Team/Cloud-imported sessions persist for aborted rounds.In the chat itself, a loaded turn whose only items were lifecycle markers also cleared the body-count threshold.
Solution
One theme: a round's
bodyEventCountis 0 exactly when nothing in it renders, and the chat treats such historical rounds as part of the surrounding conversation. Counters may still over-count; they must never under-count and hide a real body.claude_code/history/windows/index.rs): count only lines carrying amessage/isCompactSummarykey or alocal_command/compact_boundarysubtype; skipsystembookkeeping rows unless the line also carries a user/assistant type; no floor. The byte-level JSON field matcher moved toimported_history/raw_json.rsso Codex can share it.codex/app/transcript/catalog.rs,reader.rs): count only payload types the parser renders (reasoning only with text,task_completeonly with an error); no floor;load_codex_turn_atkeeps the catalog's measured count on the previous-turn placeholder.cursor_ide/):count_turn_body_bubblescounts only bubblesbubbles_to_chunkscould render (bubble_might_render_chunk, parity-tested against the chunk builder). The summary fingerprint tag moves to v3 so cached rows recompute.session-persistence/src/turn_index.rs): rows matching the sharedcore_types::session_event::is_internal_lifecycle_action_type(now also used byderived.rs) no longer add body. Retention and the legacy status still key off any activity, so an aborted round keeps its user bubble.TURN_INDEX_VERSION14 triggers a rebuild.projectors/turn_metadata.rs): blank assistant/thinking chunks are not round body.useChatGroupsProjection.ts,GroupHeaderRenderer.tsx,PinnedTurnHeader.tsx): newChatGroupMeta.hasBody. A historical round without body gets no collapse bar and no hidden placeholder row, and the next user message drops the 24px round gap so the messages stack. The newest turn keeps the current develop rules: running turns stay inline; completed nonempty turns show their summary and default to collapsed, with the final reply visible.The counts were the invalid state, so they are fixed where they are produced; the chat already treated a zero count as "no body", and the new header spacing only reacts to that data.
Potential risks
unloadedTurn.bodyEventCountcan now be 0 for Claude Code and Codex placeholders (previously always ≥ 1). The chat, turn pagination, Org2Cloud skeletons (already skip zero-body placeholders) and the mobile bridge (pass-through) accept 0. The WorkStation replay "Messages" surface still auto-fetches placeholders regardless of count, unchanged.session_turnsrows rebuild on next open (index v14), and Cursor summary cache rows recompute (fingerprint v3). No schema change. Rollback is a revert: both caches rebuild again on the version/fingerprint mismatch.runningin opencode/zcode/windsurf histories still count; native sessions drop non-last user-only rounds from the index (existing ghost rule), so a native prompt cancelled before any reply is missing from windowed history rather than showing a bar.Verification
Rust CI fixture follow-up
The
Rust (clippy)job passed its Clippy step but failedcargo test --workspace --no-fail-fastindev_and_bundle_share_persisted_sessions_across_processes: one child reportedDatabaseBusywhile saving events. The parent created tables in SQLite's default journal mode, so both children could race to switch the same database to WAL during connection initialization. The fixture now calls the productiondatabase::db::configure_connectionbefore schema creation and before spawning either child. Concurrent writes, event read-back, and numbered-instance isolation assertions remain intact; no production persistence behavior or test retries changed.node scripts/tauri/prepare-sidecars.cjs --profile debug— passed, staging the sidecar required by the app build. An initial targeted test invocation stopped before tests because this isolated worktree lacked that sidecar.cargo test --manifest-path src-tauri/Cargo.toml -p org2 --test dev_shared_sessions -- --nocapture— 8 passed, followed by 100/100 executions of the compiled test binary with--exact dev_and_bundle_share_persisted_sessions_across_processes; every run checked both writers, read-back, and numbered-instance isolationcargo clippy --manifest-path src-tauri/Cargo.toml -p org2 --test dev_shared_sessions -- -D warnings— passed before incorporating the concurrent base-branch updategit -c core.hooksPath=.husky commit; the scopedcargo clippycheck passed3e5e51f462); the fixture fix was applied on top without rewriting published history. Database, session-persistence, runtime identity, and this fixture were byte-identical across that update. Re-rancargo test --manifest-path src-tauri/Cargo.toml -p org2 --test dev_shared_sessions -- --nocaptureon the updated head: 8 passedrustfmt --check --edition 2021 src-tauri/tests/dev_shared_sessions.rsandgit diff --check— passedConflict integration against develop
0f5dbe4597GroupHeaderRenderer.test.tssuites: bodyless-round spacing and streaming completion/expansion. Updated the lifecycle-only historical-round regression to cover both collapsed and expanded defaults.pnpm install --frozen-lockfile --ignore-scripts: installed the merged branch's exact dependencies in the isolated worktree; no manifest or lockfile changes. An initial typecheck used stale shared dependencies and could not resolve develop's newyamldependency; the fresh install resolved that environment issue.pnpm exec vitest run --config config/vitest.config.ts src/engines/ChatPanel/ChatHistory: 58 files, 550 tests passed. The initial run exposed the old expanded-tail expectation; both default settings now have coverage.pnpm run typecheck:fast: passed across the merged project.cargo test --manifest-path src-tauri/Cargo.toml -p orgtrack_core --lib -- sources::codex: 108 passed, 3 ignored, including the automatically merged Codex history tests.pnpm exec eslint --max-warnings 0 --report-unused-disable-directivesandpnpm exec oxlint -c .oxlintrc.json --max-warnings 0onsrc/engines/ChatPanel/ChatHistory/renderers/GroupHeaderRenderer.test.tsandsrc/engines/ChatPanel/ChatHistory/hooks/__tests__/useChatGroupsProjection.test.ts: passed.pnpm exec prettier --checkon the merged header test, header renderer, and grouping projection: passed. The adjusted projection test was formatted withpnpm exec prettier --write.git -c core.hooksPath=.husky commit: lint-staged and tsgo passed. The scoped Clippy hook reported issues outside staged Rust files and did not block the commit; this is not a clean Clippy result. The worktree stats trailer limitation noted below still applies.git diff --check: passed. Final scope inspection retains the original empty-turn fix; manual conflict resolution changes tests only.Original implementation verification
npx vitest run --config config/vitest.config.tson the 10 chat-history/header test files (projection, identity, cell replay, search, groups projection, turn pagination, header truncation, item renderer, newGroupHeaderRenderer.test.ts,TurnCollapsePinBar) in this PR's worktree: 10 files, 108 tests passed.npx prettier --check,npx oxlint -c .oxlintrc.json --max-warnings 0,npx eslint --max-warnings 0 --report-unused-disable-directiveson the 5 changed TS files in this worktree: clean. The pre-commit hook re-ran lint-staged, tsgo (gated to staged files) and scopedcargo clippy(core_types, org2, orgtrack_core, session_persistence); its first run caught oneexplicit_auto_derefwarning, fixed before this commit.pnpm typecheck:fast(tsgo, whole project): exit 0.cargo test -p orgtrack_core --lib -- projectors sources::imported_history sources::claude_code sources::codex sources::cursor_ide sources::workbuddy sources::kimi: 364 passed, 0 failed. New:blank_replies_and_thoughts_are_not_round_body,codex_rounds_without_rendered_output_advertise_no_body,turn_summaries_count_only_bubbles_that_render,raw_jsontests.cargo test -p orgtrack_core --lib -- sources::claude_code sources::imported_history::raw_jsonafter the last Claude change: 53 passed (bookkeeping_only_rounds_advertise_no_body).cargo test -p session_persistence --lib -- turn_index: 18 passed (newlifecycle_rows_keep_the_round_but_add_no_body).derived.rs, compiled intauri dev, which rebuilt and relaunched..gitthere); the hook itself did run.