Files
PaperClipAI/ui/src/components/task-chat/reasoning-summary.ts
T
DottaandClaude Opus 5.5 d8c47159d4 feat(task-chat): show Codex reasoning summaries as headline bullets (#19271)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The task chat shows what an agent does while it runs, including a
"Thinking" row for provider reasoning
> - Real Codex runs produced reasoning items with no text
> - So the Thinking row only says "Thinking" and never tells the user
what the agent works on
> - This pull request asks Codex for its own reasoning summaries, keeps
every summary part, and shows the parts as bullets
> - The benefit is that a user can see what the agent is thinking about,
and a collapsed live run shows the newest headline as its status line

## Linked Issues or Issue Description

Refs #19252 (same pattern: show a harness signal that Paperclip already
receives)

**Subsystem affected**
Native runner (Rust runnerd Codex provider and event normalization, JS
Codex driver) and the task chat UI (Thinking row and activity rows).

**Problem or motivation**
Codex 0.160 resolves an unset reasoning summary from the model, and
current models resolve it to `"none"`. The rollout of a real gpt-5.6-sol
run records `turn_context.summary = "none"`, and all of its reasoning
items have `summary: []`. A local A/B with `codex exec` proved the
cause: `model_reasoning_summary=none` gives no reasoning text, and
`auto` gives headlines. Also, runnerd dropped `item/reasoning/*`
notifications and ignored the reasoning item's `summary[]`.

**Proposed solution**
Send `summary: "auto"` on Codex `turn/start`. Map
`item/reasoning/summaryTextDelta` and `summaryPartAdded` to reasoning
deltas, with a blank line before each later part. Use the joined
`summary[]` as the completed item text. In the UI, split the provider
text at headline lines and show one bullet per part.

**Alternatives considered**
Use `summary: "detailed"`. That gives longer text, but `auto` lets the
provider choose the best summarizer.

**Roadmap alignment**
This makes existing provider signals visible. It adds no new protocol
event type or data model.

**Additional context**
Research note: overlooked harness signals, item 2. Claude is out of
scope here. A live Claude run showed thinking blocks with signatures and
empty text: current models default `thinking.display` to `"omitted"`.
Claude Code ignores the `showThinkingSummaries` setting in SDK mode, and
ACPX does not forward a `thinking` option, so the fix needs a
claude-agent-acp patch hunk. That patch is also changed by #19264, so
the Claude change follows after #19264 merges. The UI in this PR already
shows prose-only (Claude) summaries.

## What Changed

- Rust runnerd (`codex_provider.rs`): send `summary: "auto"` on every
Codex `turn/start`.
- Rust runnerd (`provider_events.rs`): map
`item/reasoning/summaryTextDelta` and later `summaryPartAdded` to
`item.delta` with `kind: "reasoning"` and `channel: "summary"`. Use the
joined `summary[]` as a completed reasoning item's text.
- JS Codex driver: send `summary: "auto"` on direct app-server
transports, map `summaryPartAdded`, and read `summary[]` in `itemText`.
- UI: new `reasoning-summary.ts` splits provider text into headline
sections, and `TaskChatReasoningSummary` shows one bullet per part. The
Thinking row and the activity row use the newest headline as their
one-line status. Prose-only summaries (Claude) stay as prose.
- Storybook: new "Task chat reasoning summaries" story with live,
expanded, finished and Claude states.
- Tests: Rust normalizer test, JS driver test, and splitter unit tests.

## Verification

- `cargo test -p paperclip-runner-core` (provider events and the shared
identity golden).
- `pnpm --filter @paperclipai/paperclip-runner exec vitest run
src/drivers/codex`
- `pnpm --filter @paperclipai/ui exec vitest run
src/components/task-chat src/components/transcript`
- `pnpm check:token-gates`
- Live test on a local worktree instance with a real Codex agent
(gpt-5.6-sol). The Codex rollout now records `summary: "auto"`. Run
events contain `**Assessing transaction boundary issues**`, a part
break, and `**Planning atomic claim fix and verification**`. The task
chat shows "Thought · Planning atomic claim fix and verification", and
the expanded row lists both headlines as bullets.
- Storybook: open "Tasks and conversations / Task creation and lifecycle
/ Tasks / Task chat reasoning summaries".
- Claude was not changed. A live Claude run confirmed the cause for the
follow-up (empty, signed thinking blocks).

## Risks

- Codex now writes reasoning summaries, which adds a small number of
output tokens per turn. This is the same cost as Codex CLI with its
normal default.
- The UI treats a line that is only bold text as a headline. A prose
line that is only bold text also starts a new bullet. This is display
only.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`) in Claude Code, with tool use,
code execution and browser automation. Default context window.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 11:21:50 -06:00

77 lines
3.2 KiB
TypeScript

/**
* Provider reasoning summaries arrive as sections. Codex writes each summary
* part as a bold headline followed by optional prose ("**Checking test
* layout**\n\nI'll look at…"), and the runner separates parts with a blank
* line. Claude's summarized thinking is prose without headlines. This module
* splits either shape into display sections without inventing any text.
*/
export interface ReasoningSummarySection {
/** Provider-authored headline with its markdown emphasis removed. */
headline: string | null;
/** Remaining provider-authored prose for the section, if any. */
body: string;
}
const HEADLINE = /^\s*(?:\*\*|__)(.+?)(?:\*\*|__)\s*$/;
/** CommonMark fence opener: a backtick fence's info string has no backticks. */
const FENCE_OPEN = /^ {0,3}(?:(`{3,})[^`]*|(~{3,}).*)$/;
/** CommonMark fence closer: only the fence characters, then spaces. */
const FENCE_CLOSE = /^ {0,3}(`{3,}|~{3,})\s*$/;
/** A headline whose closing emphasis has not streamed in yet. */
const STREAMING_HEADLINE = /^\s*(?:\*\*|__)([^*_]+)$/;
function cleanHeadline(text: string): string {
return text.replace(/[*`]+/g, "").replace(/^_+|_+$/g, "").replace(/\s+/g, " ").trim();
}
/** Split reasoning text into headline sections; a headline line always starts a new section. */
export function reasoningSummarySections(text: string): ReasoningSummarySection[] {
const sections: ReasoningSummarySection[] = [];
let current: { headline: string | null; body: string[] } | null = null;
const flush = () => {
if (!current) return;
const body = current.body.join("\n").trim();
if (current.headline || body) sections.push({ headline: current.headline, body });
current = null;
};
const lines = text.split("\n");
let fence: string | null = null;
for (const [index, line] of lines.entries()) {
// Lines inside a fenced code block are code, never section headlines.
const openBefore = fence;
if (fence) {
const closer = FENCE_CLOSE.exec(line)?.[1];
if (closer && closer[0] === fence[0] && closer.length >= fence.length) fence = null;
} else {
const opener = FENCE_OPEN.exec(line);
if (opener) fence = opener[1] ?? opener[2] ?? null;
}
if (openBefore || fence) {
if (!current) current = { headline: null, body: [] };
current.body.push(line);
continue;
}
const match = HEADLINE.exec(line) ?? (index === lines.length - 1 ? STREAMING_HEADLINE.exec(line) : null);
const headline = match ? cleanHeadline(match[1] ?? "") : "";
if (headline) {
flush();
current = { headline, body: [] };
continue;
}
if (!current) current = { headline: null, body: [] };
current.body.push(line);
}
flush();
return sections;
}
/** Whether the provider wrote section headlines (Codex) rather than plain prose. */
export function hasReasoningHeadlines(sections: readonly ReasoningSummarySection[]): boolean {
return sections.some((section) => section.headline);
}
/** The newest provider headline, used as a live run's one-line status. */
export function latestReasoningHeadline(text: string): string | null {
return reasoningSummarySections(text).findLast((section) => section.headline)?.headline ?? null;
}