Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
8624ad1
fix(chat): preserve and render complete agent turn traces
senamakel Aug 31, 2026
df9a756
Merge remote-tracking branch 'upstream/main' into agent-turn-transcript
senamakel Aug 31, 2026
dedc62c
fix(chat): render final assistant result once
senamakel Aug 31, 2026
6651d3d
fix(chat): keep responses whole and align composer controls
senamakel Aug 31, 2026
f6fd9a1
fix(chat): infer descriptive labels for degraded tools
senamakel Aug 31, 2026
8284e63
fix(chat): coalesce persisted assistant turn segments
senamakel Aug 31, 2026
5046531
fix(auth): preserve local session during login handoff
senamakel Aug 31, 2026
2f4cc97
refactor(chat): read settled transcripts directly from core
senamakel Aug 31, 2026
cb1f26f
refactor(chat): unify tool calls on assistant ui
senamakel Aug 31, 2026
579af5d
refactor(chat): remove legacy subagent renderers
senamakel Aug 31, 2026
ad8251e
fix(chat): keep message paint stable while scrolling
senamakel Aug 31, 2026
7049eb6
chore(deps): point tinyagents at reasoning persistence
senamakel Aug 31, 2026
f630f8c
fix(core): satisfy transcript delivery clippy gates
senamakel Aug 31, 2026
b6ab5ee
chore(deps): update tinyagents reasoning fix
senamakel Aug 31, 2026
9d7d595
test(chat): cover collapsed delegation consumers
senamakel Aug 31, 2026
d9d9182
Merge branch 'main' into agent-turn-transcript
M3gA-Mind Sep 1, 2026
9903997
Merge branch 'main' into agent-turn-transcript
M3gA-Mind Sep 2, 2026
2c6c076
fix(chat): report terminal tool and delegation states faithfully
M3gA-Mind Sep 2, 2026
da5c45d
fix(chat): keep a cancelled tool off the success icon, and type the n…
M3gA-Mind Sep 2, 2026
06c69ad
Merge upstream/main into agent-turn-transcript
YellowSnnowmann Sep 2, 2026
145c7a4
fix(chat): keep scoped deliveries standalone and pair orphan trails o…
YellowSnnowmann Sep 2, 2026
b83d1ae
fix(chat): page through the derived transcript instead of stopping at…
YellowSnnowmann Sep 2, 2026
f456863
fix(auth): do not clear the local-token marker on a pre-store snapshot
YellowSnnowmann Sep 2, 2026
1eec16f
Merge upstream/main into agent-turn-transcript
YellowSnnowmann Sep 2, 2026
9097699
style(e2e): prettier-format five Playwright specs inherited from main
YellowSnnowmann Sep 2, 2026
7d68ffe
test(observability): isolate the wallet paging tests on a private Sen…
YellowSnnowmann Sep 2, 2026
ffb89c8
Merge upstream/main into agent-turn-transcript
YellowSnnowmann Sep 2, 2026
1c402c8
style(e2e): prettier-format Playwright specs inherited from main
YellowSnnowmann Sep 2, 2026
77d1e10
Merge upstream/main into agent-turn-transcript
M3gA-Mind Sep 2, 2026
0fdc3f3
test(observability): keep main's file-wide reporting lock alongside t…
M3gA-Mind Sep 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/src/components/ai-elements/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
* this product's real transcript surfaces and deleted rather than force-fitted:
*
* - `Reasoning` / `ChainOfThought` — this product renders the agent's thinking
* INLINE at the position it streamed (`ToolTimelineBlock`'s `ThoughtBlock`,
* INLINE at the position it streamed (the assistant-ui delegation transcript,
* explicitly "no heading, no collapse"; `ProcessingTranscriptView`'s
* interleaved narration). Both upstream components are whole-panel
* collapsibles that hide that trail behind one "Thought for N seconds"
Expand Down
10 changes: 4 additions & 6 deletions app/src/components/assistant-ui/thread.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -651,7 +651,7 @@ const ComposerAction: FC<{
type="button"
variant="default"
size="icon"
className="aui-composer-cancel size-7 rounded-full"
className="aui-composer-cancel size-7 rounded-full bg-primary-500 text-content-inverted hover:bg-primary-600"
data-testid="stop-generation-button"
aria-label="Stop generating">
<SquareIcon className="aui-composer-cancel-icon size-3.5 fill-current" />
Expand Down Expand Up @@ -706,7 +706,7 @@ const AssistantMessage: FC = () => {
data-slot="aui_assistant-message-root"
data-role="assistant"
data-testid="agent-message"
className="fade-in slide-in-from-bottom-1 animate-in relative -mb-7.5 pb-7.5 duration-150 [contain-intrinsic-size:auto_200px] [content-visibility:auto]">
className="fade-in slide-in-from-bottom-1 animate-in relative -mb-7.5 pb-7.5 duration-150">
{/*
* One vertical rhythm for the whole message, rather than each part
* bringing its own margin. Measured before this change the gaps ran
Expand Down Expand Up @@ -877,7 +877,7 @@ const UserMessage: FC = () => {
return (
<MessagePrimitive.Root
data-slot="aui_user-message-root"
className="fade-in slide-in-from-bottom-1 animate-in grid auto-rows-auto grid-cols-[minmax(72px,1fr)_auto] content-start gap-y-2 px-2 duration-150 [contain-intrinsic-size:auto_200px] [content-visibility:auto] [&:where(>*)]:col-start-2"
className="fade-in slide-in-from-bottom-1 animate-in grid auto-rows-auto grid-cols-[minmax(72px,1fr)_auto] content-start gap-y-2 px-2 duration-150 [&:where(>*)]:col-start-2"
data-role="user">
<UserMessageAttachments />

Expand Down Expand Up @@ -939,9 +939,7 @@ const UserActionBar: FC = () => {

const EditComposer: FC = () => {
return (
<MessagePrimitive.Root
data-slot="aui_edit-composer-wrapper"
className="flex flex-col px-2 [contain-intrinsic-size:auto_200px] [content-visibility:auto]">
<MessagePrimitive.Root data-slot="aui_edit-composer-wrapper" className="flex flex-col px-2">
<ComposerPrimitive.Root className="aui-edit-composer-root border-border/60 dark:border-muted-foreground/15 ms-auto flex w-full max-w-[85%] cursor-text flex-col rounded-(--composer-radius) border bg-(--composer-bg)">
<ComposerPrimitive.Input
className="aui-edit-composer-input text-foreground min-h-14 w-full resize-none bg-transparent px-4 pt-3 pb-1 text-base outline-hidden"
Expand Down
7 changes: 0 additions & 7 deletions app/src/features/conversations/Conversations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,6 @@ import {
clearRuntimeForThread,
clearThreadSendPending,
enqueueFollowup,
fetchAndHydrateDerivedTranscript,
fetchAndHydrateTurnState,
hydrateThreadUsage,
markThreadSendPending,
Expand Down Expand Up @@ -730,12 +729,6 @@ const Conversations = ({
if (selectedThreadId) {
void dispatch(loadThreadMessages(selectedThreadId));
void dispatch(fetchAndHydrateTurnState(selectedThreadId));
// Per-turn history: each past answer's own process trail. Phase C derives
// this from the append-only transcript projection
// (`threads_transcript_get`), auto-falling back to the legacy
// `turn_state_history` hydration when the derived path is off, errors, or
// the thread has no persisted transcript (legacy thread).
void dispatch(fetchAndHydrateDerivedTranscript(selectedThreadId));
void threadApi
.getTaskBoard(selectedThreadId)
.then(board => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import {
formatTimelineEntry,
} from '../../../utils/toolTimelineFormatting';
import { AgentSparkIcon } from './AgentTimelineRail';
import { AssistantUiSubagentCall } from './AssistantUiSubagentCall';
import { ProcessingTranscriptView } from './ProcessingTranscriptView';
import { SubagentActivityBlock } from './SubagentActivityBlock';
import { ToolTimelineBlock } from './ToolTimelineBlock';

const log = createDebug('app:conversations:agent-process-source');
Expand Down Expand Up @@ -176,7 +176,7 @@ export function AgentProcessSourcePanel({
{scopedEntry ? (
// Scoped to one step: show only that step's details.
scopedEntry.subagent ? (
<SubagentActivityBlock subagent={scopedEntry.subagent} />
<AssistantUiSubagentCall activity={scopedEntry.subagent} />
) : scopedDetail ? (
<pre className="max-h-[60vh] overflow-y-auto rounded-lg bg-surface-muted px-3 py-2 text-[12px] whitespace-pre-wrap wrap-break-word text-content-secondary">
{scopedDetail}
Expand All @@ -195,7 +195,7 @@ export function AgentProcessSourcePanel({
<ProcessingTranscriptView
transcript={transcript}
entries={entries}
renderSubagent={subagent => <SubagentActivityBlock subagent={subagent} />}
renderSubagent={subagent => <AssistantUiSubagentCall activity={subagent} />}
/>
) : entries.length > 0 ? (
// Legacy snapshot (no transcript): fall back to the tool timeline,
Expand Down Expand Up @@ -223,7 +223,7 @@ export function AgentProcessSourcePanel({
<p className="text-[12px] font-medium text-content-secondary">
{formatTimelineEntry(entry).title}
</p>
<SubagentActivityBlock subagent={entry.subagent!} />
<AssistantUiSubagentCall activity={entry.subagent!} />
</div>
))}
</div>
Expand Down
227 changes: 227 additions & 0 deletions app/src/features/conversations/components/AssistantUiSubagentCall.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
import { CheckIcon, ChevronDownIcon, CircleXIcon, Loader2Icon, WorkflowIcon } from 'lucide-react';

import { cn } from '../../../components/assistant-ui/lib/utils';
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from '../../../components/assistant-ui/ui/collapsible';
import Badge from '../../../components/ui/Badge';
import WorktreeActions from '../../../components/worktree/WorktreeActions';
import { useT } from '../../../lib/i18n/I18nContext';
import type {
SubagentActivity,
SubagentToolCallEntry,
SubagentTranscriptItem,
} from '../../../store/chatRuntimeSlice';
import { basename } from '../../../utils/pathUtils';
import { stripToolCallEnvelopes } from '../../../utils/toolTimelineFormatting';
import { BubbleMarkdown } from './AgentMessageBubble';
import { AssistantUiToolCallCard } from './AssistantUiToolCall';

type ChildToolCall = SubagentToolCallEntry | Extract<SubagentTranscriptItem, { kind: 'tool' }>;

function ChildToolCallCard({ call }: { call: ChildToolCall }) {
return (
<AssistantUiToolCallCard
toolName={call.toolName}
args={call.args}
result={call.result}
status={call.status}
displayName={call.displayName}
detail={call.detail}
elapsedMs={call.elapsedMs}
failure={call.failure}
/>
);
}

function Thought({ text }: { text: string }) {
const clean = stripToolCallEnvelopes(text).trim();
if (!clean) return null;
return (
<div
data-testid="subagent-thought"
className="my-0.5 wrap-break-word [&_.prose]:text-[12px] [&_.prose]:leading-relaxed [&_.prose]:text-content-muted [&_.prose_strong]:text-content-muted [&_.prose_:is(h1,h2,h3,h4,h5,h6)]:text-[12px] [&_.prose_:is(h1,h2,h3,h4,h5,h6)]:text-content-muted">
<BubbleMarkdown content={clean} />
</div>
);
}

function SubagentDetails({
subagent,
onView,
}: {
subagent: SubagentActivity;
onView?: () => void;
}) {
const { t } = useT();
const headerBits: string[] = [];
if (subagent.mode) headerBits.push(subagent.mode);
if (subagent.dedicatedThread) headerBits.push(t('conversations.toolTimeline.workerThread'));
if (subagent.childIteration != null) {
headerBits.push(
subagent.childMaxIterations != null
? `${t('conversations.toolTimeline.turn')} ${subagent.childIteration}/${subagent.childMaxIterations}`
: `${t('conversations.toolTimeline.step')} ${subagent.childIteration}`
);
} else if (subagent.iterations != null) {
headerBits.push(
subagent.iterations === 1
? `${subagent.iterations} ${t('chat.turn')}`
: `${subagent.iterations} ${t('chat.turns')}`
);
}
if (subagent.elapsedMs != null) {
headerBits.push(
subagent.elapsedMs >= 1000
? `${(subagent.elapsedMs / 1000).toFixed(1)}s`
: `${subagent.elapsedMs}ms`
);
}
const transcript = subagent.transcript ?? [];

return (
<div
className="mt-1 space-y-0.5 text-[12px] text-content-muted"
data-testid="subagent-activity">
{headerBits.length > 0 ? (
<div className="flex flex-wrap items-center gap-1.5">
{headerBits.map(bit => (
<Badge key={bit} className="rounded-full">
{bit}
</Badge>
))}
</div>
) : null}
{transcript.length > 0 ? (
<div className="ml-1 space-y-0.5" data-testid="subagent-transcript">
{transcript.map((item, index) =>
item.kind === 'tool' ? (
<ChildToolCallCard key={item.callId} call={item} />
) : (
<Thought key={`thought-${index}`} text={item.text} />
)
)}
</div>
) : subagent.toolCalls.length > 0 ? (
<div className="ml-1 space-y-0.5">
{subagent.toolCalls.map(call => (
<ChildToolCallCard key={call.callId} call={call} />
))}
</div>
) : null}
{subagent.worktreePath ? (
<div
className="mt-1 space-y-1 rounded-md border border-line bg-surface-muted/70 p-1.5"
data-testid="subagent-worktree">
<div className="flex flex-wrap items-center gap-1.5">
<span className="font-medium text-content-secondary">{t('worktree.label')}</span>
<span
className="truncate font-mono text-[12px] text-content-muted"
title={subagent.worktreePath}>
{basename(subagent.worktreePath)}
</span>
<Badge variant={subagent.isDirty ? 'warning' : 'success'} className="rounded-full">
{subagent.isDirty ? t('worktree.dirty') : t('worktree.clean')}
</Badge>
{subagent.changedFiles?.length ? (
<span className="text-[11px] text-content-faint">
{subagent.changedFiles.length}{' '}
{subagent.changedFiles.length === 1
? t('worktree.changedFile')
: t('worktree.changedFiles')}
</span>
) : null}
</div>
<WorktreeActions path={subagent.worktreePath} isDirty={subagent.isDirty} compact />
</div>
) : null}
{onView ? (
<button
type="button"
onClick={onView}
data-testid="subagent-view-processing"
className="mt-0.5 rounded-full px-1.5 py-0.5 text-[12px] font-medium text-primary-600 hover:bg-primary-50 dark:text-primary-300 dark:hover:bg-primary-500/15">
{t('conversations.subagent.viewProcessing')} →
</button>
) : null}
</div>
);
}

/**
* Statuses that mean the delegation is still in flight.
*
* `SubagentActivity.status` carries `running` | `awaiting_user` | `completed` |
* `failed`, and collapsing that to a boolean is what produced two opposite
* rendering bugs: a caller that omitted `running` showed a *failed* delegation
* with a success check, while `status !== 'completed'` gave the same row an
* endless spinner. Both call sites now ask this one question.
*/
export function isActiveSubagentStatus(status: string | undefined): boolean {
return status === 'running' || status === 'awaiting_user';
}

/** Statuses that mean the delegation stopped without succeeding. */
function isFailedSubagentStatus(status: string | undefined): boolean {
return status === 'failed' || status === 'cancelled';
}

export function AssistantUiSubagentCall({
activity,
running,
description,
onView,
defaultOpen = false,
}: {
activity: SubagentActivity;
running?: boolean;
description?: string;
onView?: () => void;
defaultOpen?: boolean;
}) {
const name = activity.displayName ?? activity.agentId ?? 'subagent';
// Default to the activity's own lifecycle rather than `false`: most call
// sites pass no `running` prop at all, and treating every non-running
// activity as finished-successfully is what rendered a failed delegation
// with a success check.
const active = running ?? isActiveSubagentStatus(activity.status);
const failed = !active && isFailedSubagentStatus(activity.status);
return (
<Collapsible
defaultOpen={defaultOpen}
data-slot="aui_subagent-call"
data-testid="assistant-ui-subagent-call"
data-status={activity.status ?? (active ? 'running' : 'completed')}
className={cn(
'aui-subagent-call border-border/60 dark:border-muted-foreground/15 rounded-xl border',
active && 'border-dashed'
)}>
<CollapsibleTrigger className="group/subagent text-muted-foreground hover:text-foreground flex w-full items-center gap-2 px-3 py-2 text-sm transition-colors">
<WorkflowIcon className="size-4 shrink-0" />
<span className="text-start leading-none">
Delegated to <b className="text-foreground">{name}</b>
</span>
{active ? (
<span className="bg-muted text-muted-foreground flex shrink-0 items-center gap-1.5 rounded-full px-2 py-0.5 text-[11px] leading-none">
<Loader2Icon className="size-3 animate-spin [animation-duration:0.6s]" /> running
</span>
) : (
<span className="text-muted-foreground flex shrink-0 items-center gap-1.5 text-[11px] leading-none">
{failed ? <CircleXIcon className="size-3.5" /> : <CheckIcon className="size-3.5" />}
{failed ? <span>{activity.status}</span> : null}
{activity.elapsedMs != null ? (
<span className="tabular-nums">{(activity.elapsedMs / 1000).toFixed(1)}s</span>
) : null}
</span>
)}
<ChevronDownIcon className="ml-auto size-4 shrink-0 -rotate-90 transition-transform group-data-[state=open]/subagent:rotate-0" />
</CollapsibleTrigger>
<CollapsibleContent className="px-3 pb-3">
{description ? <p className="text-muted-foreground text-xs">{description}</p> : null}
<SubagentDetails subagent={activity} onView={onView} />
</CollapsibleContent>
</Collapsible>
);
}
Loading
Loading