attribute sidechain prose/thinking to the agent, not claude (T-265); close T-267
A sub-agent's prose rendered as "claude" and its thinking as "thinking", identical to the main assistant, because the label logic ignored isSidechain — presenting sub-agent output as if the main Claude said it. Now a sidechain AssistantTextMessage is labelled "agent" with a muted stripe (never the coral claudeAccent brand), and sidechain thinking is "agent thinking". Main-thread items are unchanged. Tests: sidechain prose/thinking relabel, main-thread unchanged; golden contrasting the muted agent stripe with the coral claude stripe. This completes epic T-267 (conversation rendering streamlining): fold success result (T-262), fold agent prompt (T-263), nest the agent run (T-264), the shared holder primitive (T-266), and this attribution fix. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -819,3 +819,6 @@ INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by,
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-266', 'status', 'in_progress', 'done', NULL, '2026-06-08 09:04:12', '2026-06-08 09:04:12', '2026-06-08 09:04:12', NULL, 'fecc7bc78e18d5dd3c613d6583371fc3', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-264', 'status', 'backlog', 'in_progress', NULL, '2026-06-08 09:07:02', '2026-06-08 09:07:02', '2026-06-08 09:07:02', NULL, 'd82d792cf5ef8e77c581bea27f2cd8e3', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-264', 'status', 'in_progress', 'done', NULL, '2026-06-08 09:25:50', '2026-06-08 09:25:50', '2026-06-08 09:25:50', NULL, '274e5d99e4db192a6b4cf6902f12df45', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-265', 'status', 'backlog', 'in_progress', NULL, '2026-06-08 09:27:49', '2026-06-08 09:27:49', '2026-06-08 09:27:49', NULL, '60970c3937a5ecf1c99960aa0f3f40d8', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-265', 'status', 'in_progress', 'done', NULL, '2026-06-08 10:09:14', '2026-06-08 10:09:14', '2026-06-08 10:09:14', NULL, 'e737c0595ca3bb11cd6dbf3f1b4a914c', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-267', 'status', 'in_progress', 'done', NULL, '2026-06-08 10:09:21', '2026-06-08 10:09:21', '2026-06-08 10:09:21', NULL, '4e89e3629e224a95f2a3edf0ed4317c4', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
|
||||
@@ -1972,3 +1972,40 @@ Scope:
|
||||
Depends on T-263 (parentUuid parsing + Agent card as container). Pairs with the sidechain-prose relabel ticket. Note the holder-card restyle + auto-scroll race is a separate design discussion (see the holder-card ticket).
|
||||
|
||||
Refs: D-78. Builds on T-263. Related: T-262, T-230 (activity card grouping).', 'done', 'medium', NULL, NULL, 'D-78', '2026-06-07 08:40:25', '2026-06-08 09:25:50', NULL, 'e3afb1ab7a0530c0b8dbd5775cc3803d', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-265', 'story', 'T-267', 'Relabel sidechain assistant prose/thinking — attribute to agent, not claude', 'Mirror of T-263 on the OUTPUT side. A sub-agent runs as a sidechain; its assistant prose renders with the label "claude" and its thinking with "thinking", identically to the MAIN assistant — because the label logic does not check isSidechain. So a sub-agents output is presented as if the main Claude said it. Same misattribution T-263 fixes for the prompt (the "you" problem), mirrored on the assistant side.
|
||||
|
||||
Current code (lib/builtin/claude/src/conversation_view.dart _ConversationTurn build ~L221): AssistantTextMessage → ConversationCard(accent: claudeAccent, label: "claude"); AssistantThinkingMessage (~L227) → label "thinking". Neither inspects ConversationItem.isSidechain (transcript_reader.dart ~L46), which is present but unused.
|
||||
|
||||
Design: when isSidechain is true, attribute the assistant items to the AGENT rather than the main Claude — e.g. label "agent" (or the agent/sub-agent name where resolvable) with de-emphasised styling, NOT the coral "claude" brand accent (claudeAccent is the main-thread brand, reserved for the real Claude). Thinking inside a sidechain attributed/muted likewise.
|
||||
|
||||
Scope:
|
||||
- Branch the AssistantTextMessage / AssistantThinkingMessage rendering on isSidechain.
|
||||
- Pick the sidechain label + styling (distinct from "claude"; reuse muted tokens, not claudeAccent).
|
||||
- Keep behavior identical for main-thread items.
|
||||
|
||||
Tests: relabel of sidechain assistant prose/thinking (never "claude"/coral); main-thread unchanged; golden for an attributed agent prose card.
|
||||
|
||||
Pairs with the nest-the-agent-run ticket (attribution + containment) and T-263 (prompt side). Refs: D-78.', 'in_progress', 'medium', NULL, NULL, 'D-78', '2026-06-07 08:40:33', '2026-06-08 09:27:49', NULL, '450360f9eec6f076e6db381633ce253c', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-265', 'story', 'T-267', 'Relabel sidechain assistant prose/thinking — attribute to agent, not claude', 'Mirror of T-263 on the OUTPUT side. A sub-agent runs as a sidechain; its assistant prose renders with the label "claude" and its thinking with "thinking", identically to the MAIN assistant — because the label logic does not check isSidechain. So a sub-agents output is presented as if the main Claude said it. Same misattribution T-263 fixes for the prompt (the "you" problem), mirrored on the assistant side.
|
||||
|
||||
Current code (lib/builtin/claude/src/conversation_view.dart _ConversationTurn build ~L221): AssistantTextMessage → ConversationCard(accent: claudeAccent, label: "claude"); AssistantThinkingMessage (~L227) → label "thinking". Neither inspects ConversationItem.isSidechain (transcript_reader.dart ~L46), which is present but unused.
|
||||
|
||||
Design: when isSidechain is true, attribute the assistant items to the AGENT rather than the main Claude — e.g. label "agent" (or the agent/sub-agent name where resolvable) with de-emphasised styling, NOT the coral "claude" brand accent (claudeAccent is the main-thread brand, reserved for the real Claude). Thinking inside a sidechain attributed/muted likewise.
|
||||
|
||||
Scope:
|
||||
- Branch the AssistantTextMessage / AssistantThinkingMessage rendering on isSidechain.
|
||||
- Pick the sidechain label + styling (distinct from "claude"; reuse muted tokens, not claudeAccent).
|
||||
- Keep behavior identical for main-thread items.
|
||||
|
||||
Tests: relabel of sidechain assistant prose/thinking (never "claude"/coral); main-thread unchanged; golden for an attributed agent prose card.
|
||||
|
||||
Pairs with the nest-the-agent-run ticket (attribution + containment) and T-263 (prompt side). Refs: D-78.', 'done', 'medium', NULL, NULL, 'D-78', '2026-06-07 08:40:33', '2026-06-08 10:09:14', NULL, 'cfed081ba171d068e3ab6a2016c9c6d4', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-267', 'epic', NULL, 'Conversation rendering streamlining — fold, attribute, contain', 'Home for the cohesive set of work that streamlines how the Claude conversation log renders, so a heavy agent turn reads clearly and nothing is mislabeled or duplicated. Three recurring moves bind these tickets: (1) FOLD a redundant standalone item into its owning card (success result → tool card; agent prompt → agent card); (2) fix MISLEADING ATTRIBUTION (sub-agent prompt shown as "you"; sub-agent prose shown as "claude"); (3) settle the CONTAINER / interaction model (holder card that contains sub-cards, nested agent run, autoscroll-vs-reach-the-control).
|
||||
|
||||
Common substrate across the children: the flat ConversationItem list and its renderers in lib/builtin/claude/src/conversation_view.dart, the pure grouping pass in activity_cluster.dart, the ConversationCard template in conversation_card.dart, and the so-far-unused ConversationItem.isSidechain / parentUuid transcript fields (transcript_reader.dart).
|
||||
|
||||
Children: T-262 (merge tool-call + success result into one card with header status check), T-263 (fold sub-agent prompt into the Agent card; relabel you → agent prompt), T-264 (nest the whole sub-agent run under its Agent card via parentUuid), T-265 (relabel sidechain assistant prose/thinking — agent, not claude), T-266 (restyle activity/holder card as a container of sub-cards + fix the collapse-control scroll race). Sequencing: T-262/T-263 first, T-264/T-265 build the sidechain story, T-266 settles the shared container model last.
|
||||
|
||||
Refs: D-78 (interaction zone / display-only conversation widgets). Built on T-168 (per-tool body rendering) and T-230 (activity card).
|
||||
|
||||
SEQUENCING UPDATE (after T-266 refinement): the shared container/holder primitive now lives in T-266 and is CONSUMED by T-264 (nested agent run), so T-266 is no longer "last" — its primitive lands before/with T-264. T-264 is now blocked by T-266. Revised order: T-262 / T-263 (fold success result, fold agent prompt) → T-266 (shared holder/container primitive + activity-card restyle) → T-264 (nest the whole agent run on that primitive) → T-265 (relabel sidechain prose) can land anytime alongside.', 'done', 'medium', NULL, NULL, 'D-78', '2026-06-07 08:42:53', '2026-06-08 10:09:21', NULL, 'bb82ba1e08ca8009613521e59e548e77', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
|
||||
@@ -144,6 +144,10 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit.
|
||||
|
||||
### Changed
|
||||
|
||||
- A **sub-agent's prose and thinking** are now attributed to the **`agent`** (a
|
||||
muted stripe), not the main-thread coral **`claude`** — so a sub-agent's
|
||||
output is no longer presented as if the main Claude said it. Main-thread items
|
||||
are unchanged. (T-265)
|
||||
- A **sub-agent's whole run** — its prose, thinking, and tool calls — now nests
|
||||
in an **`agent run` holder under its Agent card** instead of spilling loose
|
||||
into the main thread, so you can tell where the sub-agent's work begins and
|
||||
|
||||
@@ -354,16 +354,19 @@ class _ConversationTurn extends StatelessWidget {
|
||||
copyText: i.text,
|
||||
body: ClideMarkdown(i.text),
|
||||
),
|
||||
// Sub-agent (sidechain) prose is NOT the main Claude — attribute it to the
|
||||
// agent with a muted accent, never the coral "claude" brand (T-265). The
|
||||
// coral claudeAccent is reserved for the real main-thread Claude.
|
||||
AssistantTextMessage() => ConversationCard(
|
||||
accent: claudeAccent,
|
||||
label: 'claude',
|
||||
accent: i.isSidechain ? tokens.globalTextMuted : claudeAccent,
|
||||
label: i.isSidechain ? 'agent' : 'claude',
|
||||
copyText: i.text,
|
||||
body: ClideMarkdown(i.text),
|
||||
),
|
||||
AssistantThinkingMessage() => ConversationCard(
|
||||
variant: ConversationCardVariant.bare,
|
||||
accent: tokens.globalTextMuted,
|
||||
label: 'thinking',
|
||||
label: i.isSidechain ? 'agent thinking' : 'thinking',
|
||||
copyText: i.thinking,
|
||||
collapsible: true,
|
||||
collapsedByDefault: true,
|
||||
|
||||
@@ -393,6 +393,32 @@ void main() {
|
||||
expect(find.bySemanticsLabel('agent run, 2 steps, collapsed'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('sidechain assistant prose is attributed to "agent", not "claude" (T-265)', (tester) async {
|
||||
// An orphan sidechain prose (no resolvable Agent) renders inline, still
|
||||
// attributed to the agent — never the main-thread coral "claude".
|
||||
await pumpWith(tester, [
|
||||
AssistantTextMessage(uuid: 's', timestamp: _t, isSidechain: true, text: 'sub-agent says hi'),
|
||||
]);
|
||||
expect(find.text('agent'), findsOneWidget);
|
||||
expect(find.text('claude'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('sidechain thinking is attributed to "agent thinking" (T-265)', (tester) async {
|
||||
await pumpWith(tester, [
|
||||
AssistantThinkingMessage(uuid: 's', timestamp: _t, isSidechain: true, thinking: 'hmm let me think'),
|
||||
]);
|
||||
expect(find.text('agent thinking'), findsOneWidget);
|
||||
expect(find.text('thinking'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('main-thread prose + thinking keep "claude"/"thinking" (T-265 unchanged)', (tester) async {
|
||||
await pumpWith(tester, [_asst('main says hi'), _think('main thought')]);
|
||||
expect(find.text('claude'), findsOneWidget);
|
||||
expect(find.text('thinking'), findsOneWidget);
|
||||
expect(find.text('agent'), findsNothing);
|
||||
expect(find.text('agent thinking'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('a permission-prompted tool-use is hidden but its result is kept', (tester) async {
|
||||
await pumpWith(
|
||||
tester,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:alchemist/alchemist.dart';
|
||||
import 'package:clide/builtin/claude/src/conversation_card.dart';
|
||||
import 'package:clide/builtin/claude/src/conversation_view.dart' show claudeAccent;
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
@@ -12,6 +13,38 @@ void main() {
|
||||
setUp(() async => f = await KernelFixture.create());
|
||||
tearDown(() async => f.dispose());
|
||||
|
||||
goldenTest(
|
||||
'ConversationCard attribution (T-265): agent prose is muted, claude prose is coral',
|
||||
fileName: 'conversation_card_attribution',
|
||||
builder: () => GoldenTestGroup(
|
||||
columns: 1,
|
||||
children: [
|
||||
GoldenTestScenario(
|
||||
name: 'claude (main thread) — coral brand stripe',
|
||||
child: _wrap(
|
||||
f,
|
||||
const ConversationCard(
|
||||
accent: claudeAccent,
|
||||
label: 'claude',
|
||||
body: Text('Here is the main-thread answer.', textDirection: TextDirection.ltr),
|
||||
),
|
||||
),
|
||||
),
|
||||
GoldenTestScenario(
|
||||
name: 'agent (sidechain) — muted stripe, not coral',
|
||||
child: _wrap(
|
||||
f,
|
||||
ConversationCard(
|
||||
accent: const Color(0xFF8B8B8B),
|
||||
label: 'agent',
|
||||
body: const Text('Here is the sub-agent answer.', textDirection: TextDirection.ltr),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
goldenTest(
|
||||
'ConversationCard merged tool card (T-262): status mark + folded result segment',
|
||||
fileName: 'conversation_card_merged',
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
Reference in New Issue
Block a user