mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-09 16:02:36 +01:00
## 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>
102 lines
4.4 KiB
TypeScript
102 lines
4.4 KiB
TypeScript
import { useState } from "react";
|
|
import { Brain, ChevronRight } from "lucide-react";
|
|
import { cn } from "@/lib/utils";
|
|
import type { TaskChatThinkingItem } from "./task-chat-model";
|
|
import { flattenSelfTalk } from "./transcript-adapter";
|
|
import { TaskChatReasoningSummary } from "./TaskChatReasoningSummary";
|
|
import { latestReasoningHeadline } from "./reasoning-summary";
|
|
import { useTaskChatShimmer } from "./TaskChatShimmer";
|
|
|
|
/** Provider-authored reasoning summaries and deltas; never reconstructed hidden reasoning. */
|
|
export function TaskChatThinking({
|
|
item,
|
|
defaultOpen,
|
|
rowClassName,
|
|
active = Boolean(item.streaming),
|
|
}: {
|
|
item: TaskChatThinkingItem;
|
|
/** Activity timelines keep reasoning as one compact row until requested. */
|
|
defaultOpen?: boolean;
|
|
/** Optional alignment override for a containing activity rail. */
|
|
rowClassName?: string;
|
|
/** Whether this reasoning block is the runner's current activity. */
|
|
active?: boolean;
|
|
}) {
|
|
const [open, setOpen] = useState(defaultOpen ?? false);
|
|
const shimmer = useTaskChatShimmer(item.id);
|
|
const body = item.lines.join("\n").trim();
|
|
const preview = latestReasoningHeadline(body) ?? flattenSelfTalk(body);
|
|
const baseLabel = item.channel === "detail" ? "Reasoning detail" : "Reasoning";
|
|
const label = active
|
|
? body ? `${baseLabel}…` : "Thinking…"
|
|
: item.summaryLabel ?? (body ? baseLabel : "Thought");
|
|
|
|
if (body && !active) {
|
|
return (
|
|
<div
|
|
className={cn(
|
|
"flex min-w-0 items-start gap-1.5 px-1 py-0.5 text-xs font-normal text-muted-foreground",
|
|
rowClassName,
|
|
)}
|
|
data-testid="task-chat-thinking"
|
|
data-state="settled"
|
|
>
|
|
<span className="flex w-5 shrink-0 items-center justify-center">
|
|
<Brain
|
|
className="mt-0.5 h-3.5 w-3.5 shrink-0 text-muted-foreground/50"
|
|
aria-hidden
|
|
data-testid="task-chat-thinking-icon"
|
|
/>
|
|
</span>
|
|
<div className="min-w-0 flex-1" data-testid="task-chat-thinking-text">
|
|
<TaskChatReasoningSummary text={body} />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!body) {
|
|
return (
|
|
<div className={cn("-mx-1.5 flex min-w-0 items-center gap-2 px-1.5 py-0.5 text-xs font-normal text-muted-foreground", rowClassName)} data-testid="task-chat-thinking">
|
|
<span className="flex w-5 shrink-0 items-center justify-center">
|
|
<Brain className={cn("h-3.5 w-3.5 shrink-0", active && "text-(--status-agent-running)")} aria-hidden data-testid="task-chat-thinking-icon" />
|
|
</span>
|
|
<span className={cn(active && shimmer && "shimmer-text shimmer-text-muted")}>{label}</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="flex min-w-0 max-w-full flex-col overflow-hidden text-xs font-normal" data-testid="task-chat-thinking">
|
|
<button
|
|
type="button"
|
|
aria-expanded={open}
|
|
aria-label={`${open ? "Collapse" : "Expand"} ${baseLabel.toLowerCase()}: ${preview}`}
|
|
onClick={() => setOpen((value) => !value)}
|
|
className={cn("group/thinking -mx-1.5 flex w-full min-w-0 max-w-full items-center gap-2 overflow-hidden rounded-sm px-1.5 py-0.5 text-left font-normal text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", rowClassName)}
|
|
>
|
|
<span className="flex w-5 shrink-0 items-center justify-center">
|
|
<Brain className={cn("h-3.5 w-3.5 shrink-0", active && "text-(--status-agent-running)")} aria-hidden data-testid="task-chat-thinking-icon" />
|
|
</span>
|
|
{active ? (
|
|
<span className={cn("shrink-0", shimmer && "shimmer-text shimmer-text-muted")}>{label}</span>
|
|
) : null}
|
|
{!open ? (
|
|
<span
|
|
className="task-chat-collapsed-line-fade min-w-0 flex-1"
|
|
data-testid="task-chat-thinking-preview"
|
|
>
|
|
{preview}
|
|
</span>
|
|
) : null}
|
|
<ChevronRight className={cn("tc-hover-disclosure-caret h-3 w-3 shrink-0 transition-[opacity,transform] group-hover/thinking:opacity-80 group-focus-visible/thinking:opacity-80", open ? "rotate-90 opacity-80" : "opacity-0")} aria-hidden />
|
|
</button>
|
|
{open ? (
|
|
<div className="task-chat-expanded-line-wrap ml-2.5 mt-1 min-w-0 max-w-full overflow-hidden border-l-2 border-border pl-2.5 text-xs font-normal text-muted-foreground">
|
|
<TaskChatReasoningSummary text={body} streaming={active} />
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|