Files
PaperClipAI/ui/src/components/task-chat/TaskChatLiveTail.tsx
T
DottaandPaperclip 9cfa7fd2d1 fix(ui): share compact live and saved activity across runners (#13421)
## Thinking Path

> - Paperclip helps people manage AI agents and inspect their work.
> - Task threads show live activity and saved transcripts from several
runners.
> - Legacy runs used a separate activity renderer that expanded tool
cards as commands arrived.
> - A compact live row makes ongoing work easier to follow.
> - This PR shares the native runner activity group across live and
saved legacy turns.
> - Both paths now use the same labels, icon gutter, animation, and
history controls.

## Linked Issues or Issue Description

Related: Refs #13255, which introduced rolling native-runner activity
groups.

**What happened?**

During a legacy CLI run, each new command added an expanded tool card
under an activity heading. The original legacy parity story rendered a
completed turn, so it did not exercise this live path.

**Expected behavior**

Show one current activity line per commentary group. Roll that line
forward when a new activity starts. Keep tool icons aligned on the left,
use friendly labels, and show history only when expanded.

**Steps to reproduce**

1. Start a task with the Codex local adapter using the CLI engine.
2. Ask it to read two files in separate tool calls and run a test
command.
3. Watch the activity feed while the run is active, then expand its
history.

**Paperclip version or commit**

The live behavior was reproduced on
bdee5ebb21b2d09280e49c88bc329435da1b07c8. This update includes both live
and saved rendering fixes.

**Deployment mode**

Built from source with the local test-drive command and a real Codex CLI
agent.

## What Changed

- Use `TaskChatRunnerActivityGroup` for both saved legacy phases and
`TaskChatLiveTail`.
- Align the legacy Working spinner with the shared activity icon gutter.
- Cover streamed reasoning updates, successive commands, image labels,
hidden details, and explicit history expansion in tests.
- Feed raw legacy transcript events through the real adapter and live
renderer in Storybook. Add live, expanded, narrow, light, and completed
stories with the final reply preserved.

## Verification

- Passed 195 targeted tests covering the live tail, status pill, shared
activity group, native turn, and full task thread.
- Passed UI typechecking, token gates, UI build, and Storybook build
before submission.
- Observed a real Codex CLI run while it read separate files and ran
tests. The current activity stayed at one 32-pixel row without an
accumulated tool list; the spinner and activity icon centers aligned.
- Compared native and legacy Storybooks: identical rolling animation,
fixed height, persistent expansion, truncated long labels, and correct
light and completed states.
- Full repository `pnpm -r typecheck` and `pnpm build` passed after
replaying the branch on current master, including the Rust runner. The
full `pnpm test:run` suite is still running.

## Risks

- Legacy activity now starts collapsed. Users can expand each group and
each row to inspect the same transcript details.
- Live runtime request cards must retain their timeline positions.
Existing task-thread and native-runner regression tests cover this
boundary.
- No API, database, or transcript format changes.

## Model Used

- OpenAI GPT-6 through Codex for the live-path fix, tests, and browser
verification. Capabilities used: reasoning, tool use, local code
execution, and image inspection. The exact deployment ID and
context-window size are not exposed in this session.
- The original saved-turn change recorded OpenAI Codex with GPT-5.6; its
exact variant and context size were not recorded.

## 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
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-09-15 16:40:32 -05:00

108 lines
3.8 KiB
TypeScript

import type { ReactElement } from "react";
import { MarkdownBody } from "@/components/MarkdownBody";
import type { TaskChatItem, TaskChatRuntimeRequestDecision, TaskChatRuntimeRequestItem } from "./task-chat-model";
import { TaskChatToolCard } from "./TaskChatToolCard";
import { TaskChatUsageReadout } from "./TaskChatUsageReadout";
import { TaskChatRunnerActivityGroup } from "./TaskChatRunnerActivityGroup";
import { TaskChatThinking } from "./TaskChatThinking";
import { TaskChatProtocolCard } from "./TaskChatProtocolCard";
import { buildTurnTimelineRows } from "./transcript-adapter";
/**
* Live body for legacy runner transcripts. The adapter drops debug plumbing;
* the same activity group as the native runner shows one rolling current row
* with friendly tool labels and explicitly expandable history. The status pill
* above this body (`TaskChatLiveRunPill`) owns the run-status affordance.
*
* Stable assistant and runtime-request boundaries compact the rows into an
* ordered turn timeline. Commentary remains readable above the activity group
* it introduced, and resolved request receipts keep their original slot.
*/
export function TaskChatLiveTail({
items,
emptyMessage,
excludeFinal = false,
onRuntimeRequestDecision,
}: {
items: readonly TaskChatItem[];
/** Shown when nothing renderable has streamed yet (queued / pre-first-token). */
emptyMessage?: string;
/** New-runner turn renders final-answer messages in its dedicated response slot. */
excludeFinal?: boolean;
onRuntimeRequestDecision?: (
item: TaskChatRuntimeRequestItem,
decision: TaskChatRuntimeRequestDecision,
) => void | Promise<void>;
}) {
const visibleItems = excludeFinal
? items.filter((item) => item.kind !== "message" || item.interstitial)
: items;
const rows = buildTurnTimelineRows(visibleItems, true)
.map((item) => renderTailRow(item, onRuntimeRequestDecision))
.filter((row): row is ReactElement => row != null);
if (rows.length === 0) {
return emptyMessage ? (
<div className="px-1 py-1 text-xs text-muted-foreground/70">{emptyMessage}</div>
) : null;
}
return <div className="flex flex-col gap-2">{rows}</div>;
}
function renderTailRow(
item: TaskChatItem,
onRuntimeRequestDecision?: (
item: TaskChatRuntimeRequestItem,
decision: TaskChatRuntimeRequestDecision,
) => void | Promise<void>,
): ReactElement | null {
switch (item.kind) {
case "message": {
// Streamed reply text (always interstitial from the transcript adapter).
// Rendered as plain markdown — the settled thread later replaces it with
// the posted comment bubble, so no author header/bubble chrome here.
const text = item.text.trim();
if (!text) return null;
return (
<div
key={item.id}
className="px-1 text-sm text-foreground/90"
data-testid="task-chat-live-text"
>
<MarkdownBody softBreaks linkIssueReferences>
{item.text}
</MarkdownBody>
</div>
);
}
case "tool":
return (
<div key={item.id}>
<TaskChatToolCard item={item} />
</div>
);
case "usage":
return (
<div key={item.id}>
<TaskChatUsageReadout item={item} />
</div>
);
case "thinking":
return <TaskChatThinking key={item.id} item={item} />;
case "activity_phase":
return (
<TaskChatRunnerActivityGroup
key={item.id}
item={item}
/>
);
case "protocol":
return <TaskChatProtocolCard key={item.id} item={item} onRuntimeRequestDecision={onRuntimeRequestDecision} />;
// Markers, interactions, briefs, statuses, turns, and dropped debug kinds
// cannot appear as direct live-tail rows.
default:
return null;
}
}