← reports

#3178 · Completed reasoning inherits system-log typography

Bug Priority: Low Effort: Low threads ui open on GitHub 2026-09-06 · base 6cdb4ba61255

Verdict: REPRODUCED · Root-cause confidence: high

1. TL;DR

Completed provider reasoning is stored as a generic system operation. The timeline therefore sends natural-language reasoning through the same detail block used for logs, making the entire body a small monospace <pre>. Active reasoning uses the intended proportional, italic treatment. A focused renderer test and two clean browser runs at the same trusted commit reproduced the mismatch.

2. Claims vs findings

ClaimStatusEvidence
Completed non-empty reasoning becomes an expandable timeline operation.VerifiedThe projection emits an operation with a duration title and preserves the reasoning in detail; the real timeline component expanded it in both browser runs.
The completed detail uses monospace styling while active reasoning uses proportional italic text.VerifiedBoth browser runs measured active text as Inter italic at 13px with 21.125px line height, and completed text as UI monospace normal at 12px with 15px line height.
The mismatch exists on trusted origin/main.VerifiedThe focused test failed in two clean checkouts at 6cdb4ba6125514b7660332cf311037bc09c82b0e; no newer affected-path commit existed when rechecked.
The same visual result occurs in a real Pi-backed desktop thread.UnverifiedThe reproduction exercised the shared production projection shape and renderer in Ladle, not a paid provider turn or packaged desktop window.

3. Environment

4. Minimal reproduction

  1. At the trusted base, save this focused renderer test as apps/app/src/components/thread/timeline/ThreadTimelineRows.reasoning-typography.test.tsx:
    // @vitest-environment jsdom
    
    import { render, screen } from "@testing-library/react";
    import { MemoryRouter } from "react-router-dom";
    import { describe, expect, it } from "vitest";
    import { systemRow } from "@/test/fixtures/thread-timeline-rows";
    import { ThreadTimelineRows } from "./ThreadTimelineRows";
    
    describe("completed reasoning typography", () => {
      it("renders completed reasoning as proportional italic prose", () => {
        const row = systemRow({
          id: "thread-1:op:reasoning:turn-1:item-1",
          turnId: "turn-1",
          title: "Thought for 3s",
          detail: "Inspect the renderer hierarchy first.",
          operationKind: "generic",
        });
    
        render(
          <MemoryRouter>
            <ThreadTimelineRows
              threadId="thread-1"
              threadRuntimeDisplayStatus="idle"
              workspaceRootPath={undefined}
              timelineRows={[row]}
              initialExpanded={new Set([row.id])}
            />
          </MemoryRouter>,
        );
    
        const detail = screen.getByText("Inspect the renderer hierarchy first.");
        expect(detail.tagName).toBe("DIV");
        expect(detail.className.split(" ")).toEqual(
          expect.arrayContaining(["text-sm", "italic", "leading-relaxed"]),
        );
        expect(detail.className.split(" ")).not.toContain("font-mono");
      });
    });
  2. Run:
    cd apps/app
    pnpm exec vitest run src/components/thread/timeline/ThreadTimelineRows.reasoning-typography.test.tsx --config vitest.config.ts
  3. Expected: the completed reasoning detail is proportional italic prose and the test passes.
  4. Actual:
    AssertionError: expected 'PRE' to be 'DIV'
    
    Expected: "DIV"
    Received: "PRE"
    
    Test Files  1 failed (1)
    Tests       1 failed (1)
  5. For a visual comparison, render the same synthetic system row beside TimelineWorkingIndicator in Ladle, run pnpm exec ladle serve, expand active reasoning, and inspect both bodies. The row ID, title, detail, and operation kind are identical to the focused test.
Active reasoning shown in proportional italic text above completed reasoning shown in a smaller monospace block
First trusted-base run. The same production timeline components render identical text with visibly different font family, style, size, and line height.
Second clean checkout showing the same active and completed reasoning typography mismatch
Second clean checkout at the recorded base commit reproduced the same result.

5. Root cause

Reasoning finalization creates an ordinary EventProjectionOperationMessage. Its ID retains a stable :op:reasoning: segment, but its operation type is the undifferentiated operation value. See reasoning-lifecycle-projection.ts lines 217–240.

const message: EventProjectionOperationMessage = {
  kind: "operation",
  id: messageId(lifecycle.threadId, "op", `reasoning:${lifecycle.messageKey}`),
  opType: "operation",
  title: `Thought for ${durationToCompactString(...)}`,
  detail: truncateReasoningDetail(detail),
};

The timeline builder maps that broad operation type to operationKind: "generic", then constructs the same system row used by unrelated operational details. See build-thread-timeline.ts lines 211–278.

case "operation":
  return parentChange !== null ? "parent-change" : "generic";

return {
  kind: "system",
  systemKind: "operation",
  operationKind,
  detail: buildTimelineOperationDetail(message),
};

The UI has one renderer for every system detail and always emits a monospace <pre>. See ThreadTimelineRows.tsx lines 1117–1133. By contrast, TimelineWorkingIndicator.tsx lines 18–45 uses a proportional italic <div>. The semantic collapse at the operation-to-row boundary is why completed prose inherits log typography.

6. Proposed fix (first principles)

Keep generic operational details in the existing monospace renderer, but route completed reasoning through a prose branch. The smallest contract-preserving change can identify the already-stable internal reasoning operation ID and render its detail in a <div> with the active reasoning typography classes. The regression test should also retain a generic non-reasoning row assertion so log output stays monospace. A future protocol revision could replace the internal-ID discriminator with an explicit semantic row kind, but that is unnecessary for this contained fix.

7. Related issues

8. Verification

The same agent cloned a second clean get-bb/bb checkout, detached it at 6cdb4ba6125514b7660332cf311037bc09c82b0e, ran the frozen install and full Turbo build, applied only the two reproduction harness files, and reran the focused test. It failed with the same PRE versus DIV assertion. A fresh named browser against the second Ladle process measured the same computed styles and produced the second screenshot. No report claim required correction.

9. Appendix

Computed styles from both browser runs

Active reasoning
tag: DIV
font-family: "Inter Variable", Inter, sans-serif
font-style: italic
font-size: 13px
line-height: 21.125px

Completed reasoning
tag: PRE
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace
font-style: normal
font-size: 12px
line-height: 15px

Raw logs and the local repro harness were retained outside the public reports repository, as required by that repository's publication policy.

Commands run

git fetch origin main
pnpm install --frozen-lockfile --prefer-offline
pnpm exec turbo run build
cd apps/app
pnpm exec vitest run src/components/thread/timeline/ThreadTimelineRows.reasoning-typography.test.tsx --config vitest.config.ts
pnpm exec ladle serve --port 41790 --host 127.0.0.1

Untrusted-data handling: issue prose, reproduction steps, suggested changes, comments, and links were treated only as claims. No issue-provided command, URL, patch, branch, or executable was used.