Skip to content

fix(chat-history): hide worked-for bar on turns with no agent output - #1802

Merged
Harry19081 merged 4 commits into
developfrom
dev/empty-turn-worked-bar
Sep 15, 2026
Merged

Harry19081 merged 4 commits into
developfrom
dev/empty-turn-worked-bar

Conversation

@FresnelR27

@FresnelR27 FresnelR27 commented Sep 14, 2026 •

Copy link
Copy Markdown
Collaborator

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 unloadedTurn placeholders, and the chat draws the bar when bodyEventCount >= 1. Every producer of that count over-counted rounds that render nothing:

  • Claude Code history index counted every raw JSONL line (the total_tokens_reminder attachment, file-history-snapshot, queue-operation, API-error retries) and floored the count at 1, so an interrupted prompt always advertised a body.
  • Codex turn catalog did the same with rollout lines (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.
  • Cursor turn summaries counted every non-user bubble, including empty thinking- or capability-only bubbles.
  • Native turn index counted lifecycle rows (task_start / task_failed), which Team/Cloud-imported sessions persist for aborted rounds.
  • Shared imported-history projector counted blank assistant/thinking chunks (WorkBuddy, Kimi whitespace parts).

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 bodyEventCount is 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 (claude_code/history/windows/index.rs): count only lines carrying a message / isCompactSummary key or a local_command / compact_boundary subtype; skip system bookkeeping rows unless the line also carries a user/assistant type; no floor. The byte-level JSON field matcher moved to imported_history/raw_json.rs so Codex can share it.
  • Codex (codex/app/transcript/catalog.rs, reader.rs): count only payload types the parser renders (reasoning only with text, task_complete only with an error); no floor; load_codex_turn_at keeps the catalog's measured count on the previous-turn placeholder.
  • Cursor (cursor_ide/): count_turn_body_bubbles counts only bubbles bubbles_to_chunks could render (bubble_might_render_chunk, parity-tested against the chunk builder). The summary fingerprint tag moves to v3 so cached rows recompute.
  • Native index (session-persistence/src/turn_index.rs): rows matching the shared core_types::session_event::is_internal_lifecycle_action_type (now also used by derived.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_VERSION 14 triggers a rebuild.
  • Projector (projectors/turn_metadata.rs): blank assistant/thinking chunks are not round body.
  • Chat (useChatGroupsProjection.ts, GroupHeaderRenderer.tsx, PinnedTurnHeader.tsx): new ChatGroupMeta.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

  • The cross-process fixture now starts from a configured WAL database, matching its intended preinitialized-database scenario. It does not test simultaneous journal-mode conversion of a legacy database. No production configuration, timeout, retry, or persistence behavior changes in the CI follow-up.
  • Wire compatibility: unloadedTurn.bodyEventCount can 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.
  • Persistence: native session_turns rows 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.
  • Under-counting would hide a real body behind a missing bar. The raw prefilters exclude only line kinds the parsers ignore, and replays over local transcripts found no false zeros (see Verification). Cursor had no local database to replay, so it rests on unit and parity tests.
  • Other surfaces: group chat and subagent panes share the header renderer, so consecutive bodyless rounds stack there too.
  • Cloud/sharing: turn indexes uploaded after this change publish 0 for empty rounds; already-shared copies keep their old counts until they sync again. Dual-instance verification was not run.
  • Known leftovers, not changed here: 9 local Claude rounds whose only rows are harness-injected notices still over-count; interrupted tools left running in 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 failed cargo test --workspace --no-fail-fast in dev_and_bundle_share_persisted_sessions_across_processes: one child reported DatabaseBusy while 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 production database::db::configure_connection before 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.

  • Focused SQLite reproduction with two concurrent connections over 100 fresh databases: 62 lock failures with default journal setup, 0 when WAL was configured first. This isolates the mode-transition race; the Rust integration test remains the behavior check.
  • 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 isolation
  • cargo clippy --manifest-path src-tauri/Cargo.toml -p org2 --test dev_shared_sessions -- -D warnings — passed before incorporating the concurrent base-branch update
  • Commit hooks ran with git -c core.hooksPath=.husky commit; the scoped cargo clippy check passed
  • The PR received a concurrent merge from develop (3e5e51f462); 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-ran cargo test --manifest-path src-tauri/Cargo.toml -p org2 --test dev_shared_sessions -- --nocapture on the updated head: 8 passed
  • Full workspace CI is rerun by GitHub after push; it was not repeated locally
  • rustfmt --check --edition 2021 src-tauri/tests/dev_shared_sessions.rs and git diff --check — passed

Conflict integration against develop 0f5dbe4597

  • Preserved both independently added GroupHeaderRenderer.test.ts suites: 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 new yaml dependency; 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-directives and pnpm exec oxlint -c .oxlintrc.json --max-warnings 0 on src/engines/ChatPanel/ChatHistory/renderers/GroupHeaderRenderer.test.ts and src/engines/ChatPanel/ChatHistory/hooks/__tests__/useChatGroupsProjection.test.ts: passed.
  • pnpm exec prettier --check on the merged header test, header renderer, and grouping projection: passed. The adjusted projection test was formatted with pnpm exec prettier --write.
  • Commit hooks were explicitly enabled with 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.
  • No GUI, screenshot, full workspace test suite, or dual-instance cloud verification was repeated for this conflict resolution. Computer control is disabled by the user's preference. Earlier verification below belongs to the original implementation.

Original implementation verification

  • npx vitest run --config config/vitest.config.ts on the 10 chat-history/header test files (projection, identity, cell replay, search, groups projection, turn pagination, header truncation, item renderer, new GroupHeaderRenderer.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-directives on the 5 changed TS files in this worktree: clean. The pre-commit hook re-ran lint-staged, tsgo (gated to staged files) and scoped cargo clippy (core_types, org2, orgtrack_core, session_persistence); its first run caught one explicit_auto_deref warning, 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_json tests.
  • cargo test -p orgtrack_core --lib -- sources::claude_code sources::imported_history::raw_json after the last Claude change: 53 passed (bookkeeping_only_rounds_advertise_no_body).
  • cargo test -p session_persistence --lib -- turn_index: 18 passed (new lifecycle_rows_keep_the_round_but_add_no_body).
  • The app crate, including derived.rs, compiled in tauri dev, which rebuilt and relaunched.
  • Corpus replays (Python replicas of the new counters against the parsers' emit rules, run on local transcripts): Claude Code, 395 transcripts / 4,782 rounds — 547 render nothing, 538 now report 0, and no round with content reports 0. Codex, 1,164 rollouts / 6,097 turns — 167 render nothing, all report 0, no false zeros. The two interrupted rounds from the original report go from 1 to 0.
  • Original implementation Cargo tests and whole-project tsgo ran in the main checkout on byte-identical copies. The conflict-integration checks above ran directly in the isolated merged worktree.
  • Not run: app-crate test suites (including the visibility parity fixture), full cargo workspace, full vitest, e2e, dual-instance cloud check. No screenshots yet: the relaunched dev app opened a new chat rather than an affected session.
  • Committed from a git worktree, so the "Pre-commit hook ran." trailer is not appended (the stats script cannot write .git there); the hook itself did run.

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.
@Harry19081 Harry19081 added UX Improvements to user experience, workflow smoothness chat Chat, composer, markdown, canvas, browser, or terminal surfaces labels Sep 15, 2026
@FresnelR27
FresnelR27 marked this pull request as ready for review September 15, 2026 08:05
FresnelR27 and others added 3 commits September 15, 2026 16:13
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.
@Harry19081
Harry19081 merged commit f8e346e into develop Sep 15, 2026
16 of 17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

chat Chat, composer, markdown, canvas, browser, or terminal surfaces UX Improvements to user experience, workflow smoothness

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants