{item.author}
@@ -93,7 +123,8 @@ function ThreadItemView({
return (
@@ -141,6 +172,9 @@ function ThreadItemView({
case "tool_activity":
return ;
+ case "progress_activity":
+ return ;
+
case "denial":
return (
@@ -311,7 +345,7 @@ export function TurnGroup({
return (
-
+
Turn {turn.ordinal} · {turn.mode} · {turn.toolCallCount} tool call
{turn.toolCallCount === 1 ? "" : "s"} ·{" "}
{new Date(turn.at).toISOString().slice(11, 19)}
diff --git a/packages/paperclip-runner/devtools/issue-thread/src/issue-thread.css b/packages/paperclip-runner/devtools/issue-thread/src/issue-thread.css
index 0e61e04906c..bbba3a05ddd 100644
--- a/packages/paperclip-runner/devtools/issue-thread/src/issue-thread.css
+++ b/packages/paperclip-runner/devtools/issue-thread/src/issue-thread.css
@@ -559,6 +559,37 @@ textarea {
min-width: 0;
}
+/*
+ * Chat messages follow the familiar assistant flow: the board's message is a
+ * compact bubble, while the assistant answer sits directly on the page. The
+ * heavier card remains available for durable control-plane records below.
+ */
+.pit-message {
+ min-width: 0;
+}
+
+.pit-message[data-role="user"] {
+ align-self: flex-end;
+ width: fit-content;
+ max-width: 85%;
+ padding: var(--pit-space-3) var(--pit-space-4);
+ border-radius: var(--pit-radius-lg);
+ background: var(--pit-surface-raised);
+}
+
+.pit-message[data-role="assistant"] {
+ padding: var(--pit-space-2) 0;
+}
+
+.pit-message .pit-card-head {
+ color: var(--pit-muted-foreground);
+}
+
+.pit-app[data-surface="chat"] .pit-turn-label,
+.pit-app[data-surface="chat"] .pit-turn-header::after {
+ display: none;
+}
+
.pit-card-head {
display: flex;
align-items: baseline;
@@ -664,6 +695,100 @@ textarea {
gap: var(--pit-space-2);
}
+/* Lightweight assistant-style reasoning/tool disclosures. */
+.pit-activity-item {
+ min-width: 0;
+ color: var(--pit-muted-foreground);
+}
+
+.pit-activity-summary {
+ display: flex;
+ align-items: center;
+ gap: var(--pit-space-2);
+ min-height: var(--pit-touch-target);
+ padding: var(--pit-space-1) 0;
+ cursor: pointer;
+ list-style: none;
+}
+
+.pit-activity-summary::-webkit-details-marker {
+ display: none;
+}
+
+.pit-activity-glyph {
+ flex: none;
+ width: var(--pit-space-5);
+ color: var(--pit-muted-foreground);
+ font-family: var(--pit-font-mono);
+ text-align: center;
+}
+
+.pit-activity-item[data-status="ok"] .pit-activity-glyph,
+.pit-activity-item[data-status="complete"] .pit-activity-glyph {
+ color: var(--pit-success);
+}
+
+.pit-activity-item[data-status="denied"] .pit-activity-glyph {
+ color: var(--pit-danger);
+}
+
+.pit-activity-item[data-status="running"] .pit-activity-glyph {
+ color: var(--pit-live);
+ animation: pit-pulse 1.6s ease-in-out infinite;
+}
+
+.pit-activity-operation {
+ flex: none;
+ color: var(--pit-foreground);
+ font-family: var(--pit-font-mono);
+ font-size: var(--pit-text-xs);
+ font-weight: 600;
+}
+
+.pit-progress-activity .pit-activity-operation {
+ font-family: var(--pit-font-sans);
+}
+
+.pit-activity-description {
+ flex: 1;
+ min-width: 0;
+ overflow: hidden;
+ color: var(--pit-muted-foreground);
+ font-size: var(--pit-text-xs);
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.pit-activity-caret {
+ flex: none;
+ color: var(--pit-faint-foreground);
+ transition: transform var(--pit-motion) ease-out;
+}
+
+.pit-activity-item[open] > .pit-activity-summary .pit-activity-caret {
+ transform: rotate(90deg);
+}
+
+.pit-activity-detail {
+ display: flex;
+ flex-direction: column;
+ gap: var(--pit-space-2);
+ margin-left: calc(var(--pit-space-5) + var(--pit-space-2));
+ padding: 0 0 var(--pit-space-3) var(--pit-space-3);
+ border-left: 1px solid var(--pit-border);
+ font-size: var(--pit-text-xs);
+}
+
+.pit-activity-detail p {
+ margin: 0;
+}
+
+.pit-activity-payloads {
+ display: grid;
+ gap: var(--pit-space-2);
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+}
+
.pit-code {
margin: 0;
padding: var(--pit-space-2);
@@ -1058,6 +1183,81 @@ textarea {
overflow-wrap: anywhere;
}
+.pit-runner-event,
+.pit-runner-group {
+ min-width: 0;
+ border: 1px solid var(--pit-border);
+ border-radius: var(--pit-radius-sm);
+ background: var(--pit-surface);
+ font-size: var(--pit-text-xs);
+}
+
+.pit-runner-event-summary,
+.pit-runner-group-summary {
+ display: flex;
+ align-items: center;
+ gap: var(--pit-space-2);
+ min-height: var(--pit-touch-target);
+ padding: var(--pit-space-2);
+ cursor: pointer;
+ list-style: none;
+ min-width: 0;
+}
+
+.pit-runner-event-summary::-webkit-details-marker,
+.pit-runner-group-summary::-webkit-details-marker {
+ display: none;
+}
+
+.pit-runner-event-summary .pit-record-detail,
+.pit-runner-group-summary .pit-record-detail {
+ flex: 1;
+ min-width: 0;
+}
+
+.pit-runner-event[open] > .pit-runner-event-summary .pit-activity-caret,
+.pit-runner-group[open] > .pit-runner-group-summary .pit-activity-caret {
+ transform: rotate(90deg);
+}
+
+.pit-runner-event-details {
+ display: grid;
+ gap: var(--pit-space-2);
+ margin: 0;
+ padding: 0 var(--pit-space-2) var(--pit-space-2);
+}
+
+.pit-runner-event-details > div {
+ display: grid;
+ grid-template-columns: minmax(0, 7rem) minmax(0, 1fr);
+ gap: var(--pit-space-2);
+}
+
+.pit-runner-event-details dt,
+.pit-runner-event-details dd {
+ margin: 0;
+ overflow-wrap: anywhere;
+}
+
+.pit-runner-event-details dt {
+ color: var(--pit-faint-foreground);
+}
+
+.pit-runner-event-details dd {
+ color: var(--pit-muted-foreground);
+ font-family: var(--pit-font-mono);
+}
+
+.pit-runner-group-events {
+ display: grid;
+ gap: var(--pit-space-2);
+ padding: 0 var(--pit-space-2) var(--pit-space-2);
+}
+
+.pit-runner-group-events .pit-runner-event {
+ background: var(--pit-surface-sunken);
+}
+
.pit-tool-group-label {
font-size: var(--pit-text-xs);
font-weight: 600;
@@ -1300,6 +1500,18 @@ textarea {
padding: var(--pit-space-4) var(--pit-space-3);
}
+ .pit-message[data-role="user"] {
+ max-width: 92%;
+ }
+
+ .pit-activity-payloads {
+ grid-template-columns: minmax(0, 1fr);
+ }
+
+ .pit-activity-description {
+ white-space: normal;
+ }
+
/*
* Three transport controls plus a progress bar do not fit on one 390px row —
* squeezing them leaves the bar as an unreadable stub. Wrap so the bar keeps
diff --git a/packages/paperclip-runner/devtools/issue-thread/stream-fixture-transport.mjs b/packages/paperclip-runner/devtools/issue-thread/stream-fixture-transport.mjs
index 97c48bec702..dcd932c07e9 100644
--- a/packages/paperclip-runner/devtools/issue-thread/stream-fixture-transport.mjs
+++ b/packages/paperclip-runner/devtools/issue-thread/stream-fixture-transport.mjs
@@ -15,6 +15,8 @@
*/
const DELTA_INTERVAL_MS = 220;
+export const PHASE7_STREAM_FIXTURE_PRIVATE_REASONING =
+ "PRIVATE reasoning text must never reach the browser.";
/** Four deltas: enough for a browser to observe growth twice over, and short. */
export const PHASE7_STREAM_FIXTURE_DELTAS = [
@@ -113,6 +115,16 @@ class StreamFixtureTransport {
method: "turn/started",
params: { threadId: "stream-fixture-thread", turn: { id: turnId, status: "inProgress" } },
});
+ await sleep(Math.floor(DELTA_INTERVAL_MS / 2));
+ if (this.#closed || this.#interrupted.has(turnId)) return;
+ this.#queue.push({
+ method: "item/reasoning/summaryTextDelta",
+ params: {
+ threadId: "stream-fixture-thread",
+ turnId,
+ delta: PHASE7_STREAM_FIXTURE_PRIVATE_REASONING,
+ },
+ });
for (const delta of PHASE7_STREAM_FIXTURE_DELTAS) {
await sleep(DELTA_INTERVAL_MS);
if (this.#closed || this.#interrupted.has(turnId)) return;
diff --git a/packages/paperclip-runner/src/issue-thread/fixtures.ts b/packages/paperclip-runner/src/issue-thread/fixtures.ts
index c2469475a78..85306f920ac 100644
--- a/packages/paperclip-runner/src/issue-thread/fixtures.ts
+++ b/packages/paperclip-runner/src/issue-thread/fixtures.ts
@@ -246,6 +246,10 @@ function baseEvidence(): Phase7EvidenceModel {
kind: "session.started",
ordinal: 1,
detail: "runnerd pid 4101 · process group 4101 · thread thr-mock-7g",
+ details: [
+ { label: "Runner", value: "runnerd" },
+ { label: "State", value: "started" },
+ ],
},
{
id: "ev-2",
@@ -253,6 +257,10 @@ function baseEvidence(): Phase7EvidenceModel {
kind: "turn.started",
ordinal: 2,
detail: "turn-1 · dynamic tools 8 exposed / 3 withheld",
+ details: [
+ { label: "State", value: "started" },
+ { label: "Tools", value: "8 exposed, 3 withheld" },
+ ],
},
{
id: "ev-3",
@@ -260,6 +268,10 @@ function baseEvidence(): Phase7EvidenceModel {
kind: "tool.dispatched",
ordinal: 5,
detail: "write_document → typed result revision 4",
+ details: [
+ { label: "Operation", value: "write_document" },
+ { label: "Result", value: "typed result revision 4" },
+ ],
},
{
id: "ev-4",
@@ -267,6 +279,10 @@ function baseEvidence(): Phase7EvidenceModel {
kind: "turn.completed",
ordinal: 9,
detail: "turn-3 · status completed · 1 tool call",
+ details: [
+ { label: "State", value: "completed" },
+ { label: "Tool calls", value: "1" },
+ ],
},
],
state: [
diff --git a/packages/paperclip-runner/src/issue-thread/issue-thread.test.ts b/packages/paperclip-runner/src/issue-thread/issue-thread.test.ts
index f3bce91c159..de331abfd1a 100644
--- a/packages/paperclip-runner/src/issue-thread/issue-thread.test.ts
+++ b/packages/paperclip-runner/src/issue-thread/issue-thread.test.ts
@@ -276,6 +276,20 @@ describe("Phase 7 live projection", () => {
},
{
id: "evidence-000002",
+ kind: "provider_event",
+ at: "2026-08-09T09:00:10.000Z",
+ turnId: "turn-1",
+ data: { event: "reasoning_delta" },
+ },
+ {
+ id: "evidence-000003",
+ kind: "provider_event",
+ at: "2026-08-09T09:00:11.000Z",
+ turnId: "turn-1",
+ data: { event: "reasoning_delta" },
+ },
+ {
+ id: "evidence-000004",
kind: "tool_call",
at: "2026-08-09T09:00:30.000Z",
turnId: "turn-1",
@@ -287,7 +301,7 @@ describe("Phase 7 live projection", () => {
},
},
{
- id: "evidence-000003",
+ id: "evidence-000005",
kind: "tool_result",
at: "2026-08-09T09:00:31.000Z",
turnId: "turn-1",
@@ -344,6 +358,18 @@ describe("Phase 7 live projection", () => {
expect(items.some((item) => item.kind === "agent_message")).toBe(true);
expect(items.some((item) => item.kind === "tool_activity")).toBe(true);
+ const progress = items.find((item) => item.kind === "progress_activity");
+ expect(progress).toMatchObject({
+ kind: "progress_activity",
+ activity: "thinking",
+ status: "complete",
+ label: "Thinking",
+ eventCount: 2,
+ });
+ expect(view.evidence.runner.filter(
+ (record) => record.details.some((entry) => entry.value === "reasoning_delta"),
+ )).toHaveLength(2);
+
// The durable comment comes from the mock record the command created,
// not from the model's prose.
const durable = items.find((item) => item.kind === "durable_comment");
diff --git a/packages/paperclip-runner/src/issue-thread/live-projection.ts b/packages/paperclip-runner/src/issue-thread/live-projection.ts
index 4a57bb5615a..2dd21f9b5d9 100644
--- a/packages/paperclip-runner/src/issue-thread/live-projection.ts
+++ b/packages/paperclip-runner/src/issue-thread/live-projection.ts
@@ -15,6 +15,7 @@ import type {
} from "../mock-core/phase7-control-plane-types.js";
import {
phase7EvidenceDetail,
+ phase7EvidenceDetails,
redactPhase7ToolResult,
} from "../phase7/evidence-redaction.js";
import type {
@@ -54,6 +55,33 @@ const DISPOSITION_OPERATIONS = new Set