file T-338: agent prompt renders as a 'you' card instead of folding into the Activity Agent card

Bug under the UI tracker epic (T-276). The sub-agent prompt leaks to the
top level as a blue 'you' UserMessage instead of folding into the Activity
Agent card (T-263 behaviour), because the fold keys off envelope
isSidechain/parentUuid and the spawning prompt's envelope arrives without
them resolving to the Agent tool_use.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-10 15:48:43 +02:00
co-authored by Claude Opus 4.8
parent c6fa384972
commit 984bf0cfaf
3 changed files with 38 additions and 0 deletions
@@ -3181,3 +3181,4 @@ INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, chang
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB1S7613SYF0M9XQT5JNWM40', 'status', 'ready', 'done', NULL, '2026-06-10 13:15:15', '2026-06-10 13:15:15', '2026-06-10 13:15:15', NULL, '9cea01c55347a9c347bd195d322dd004', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB37JZSFZKWPK9PDFYJY2YC0', 'status', 'ready', 'done', NULL, '2026-06-10 13:18:57', '2026-06-10 13:18:57', '2026-06-10 13:18:57', NULL, '02a8ea1b2623be255101f56c077f63ee', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7F2TBJJV1F2KP7XR8', 'status', 'backlog', 'ready', NULL, '2026-06-10 13:24:15', '2026-06-10 13:24:15', '2026-06-10 13:24:15', NULL, '85d0441183cc6f64fb1a956522a60f4a', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7F2TBJJV1F2KP7XR8', 'status', 'ready', 'in_progress', NULL, '2026-06-10 13:41:17', '2026-06-10 13:41:17', '2026-06-10 13:41:17', NULL, '14fa1745f31d356412fe910180183a53', 2) ON CONFLICT(hash) DO NOTHING;
+2
View File
@@ -162,3 +162,5 @@ INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB3DP48FS33CQGRDF7EB9GT0', 'T-335', '2026-06-10 13:26:45', '2026-06-10 13:26:45', NULL, '1cce32ce77997202bf74c69426c29ab9', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB3DQEMTDHF8SV27AKAB8JHW', 'T-336', '2026-06-10 13:26:56', '2026-06-10 13:26:56', NULL, 'b988abcd2cef0dbd6adbc56502f676ca', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB3DWCJSGZH9WYDNFWZBAYYR', 'T-337', '2026-06-10 13:27:36', '2026-06-10 13:27:36', NULL, '94b0ebd244a6794e0e4e5a868b38d67f', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB3JAXDKZMS0805MMEYB6820', 'T-338', '2026-06-10 13:47:04', '2026-06-10 13:47:04', NULL, 'be9b536d471a6008cc854092dfa6aaa4', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB3JM0AXK1CTWD720RV1AVZ0', 'T-339', '2026-06-10 13:48:18', '2026-06-10 13:48:18', NULL, 'e76a2ce5d3149bb7ca0ec8a45f5c57a2', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
+35
View File
@@ -2480,3 +2480,38 @@ Where to look:
Repro: enter plan mode, let Claude present a plan via ExitPlanMode, accept it the pane stays showing plan mode.
Acceptance: accepting ExitPlanMode leaves plan mode in the conversation panel (mode indicator + behavior update), matching what the underlying session does.', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-10 13:27:36', '2026-06-10 13:27:36', NULL, 'e4644bb33acffeb4ac1cdd2ab7dd0942', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_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 (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7F2TBJJV1F2KP7XR8', 'task', '06FB0TNQM5TWC00GW0P3X02HZW', 'Clickable file references in Claude conversation open in editor', 'Make file-path references rendered in the Claude conversation pane clickable: clicking a path opens that file in the editor.
**Behavior**
- Detect references to files in the workspace as the conversation markdown is rendered bare paths (`lib/app.dart`), `file:line` forms (`lib/app.dart:42`, clickable per CLAUDE.md), and likely inline-code spans / markdown links pointing at repo paths.
- Resolve against the workspace root (CLIDE_WORKSPACE / git repo root). Only linkify paths that exist in the repo to avoid false positives on prose.
- On click, open in the editor same path as `clide editor open <path>` and jump to the line when a `:line` suffix is present.
**Notes / constraints**
- Markdown rendering is clide-owned custom CustomPaint/widgets (not a package), so detection + hit-testing lands in the conversation/markdown render path.
- Honors User/Claude parity (D-6): the click maps to the existing `clide editor open` verb.
- Open questions to settle during design: how aggressive path detection should be (existence check vs. heuristic), handling of non-existent / external paths, and visual affordance (underline/hover) for a linkified ref.', 'in_progress', 'medium', NULL, NULL, NULL, '2026-06-09 20:02:41', '2026-06-10 13:41:17', NULL, '36eeacc93f291c6db429b1fa7a75c8c4', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_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 (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB3JAXDKZMS0805MMEYB6820', 'bug', '06FB0TNQM5TWC00GW0P3X02HZW', 'Agent prompt renders as a ''you'' card instead of folding into the Activity Agent card', 'When Claude spawns a sub-agent (Agent/Task tool, rendered as an ''Activity Agent'' card), the prompt handed to the sub-agent renders as a separate top-level UserMessage card labelled ''you'' (blue, globalFocus accent) above the card, instead of folding into the Activity Agent card as a muted ''agent prompt'' (the T-263 behaviour).
Repro: launch any sub-agent from the Claude pane; the task prompt appears as a ''you'' turn.
Root cause: the sidechain fold keys entirely off the transcript envelope. In transcript_reader.dart the prompt''s UserMessage carries isSidechain/parentUuid straight from envelope[''isSidechain''] and envelope[''parentUuid''] (transcript_reader.dart:558-560, parsed in _parseUserInto at :593-643). conversation_view.dart _sidechainFold() (:179-233) only claims an item when it.isSidechain is true AND resolveOwner() walks its parentUuid chain up to the Agent tool_use message (or the nearest preceding Agent as fallback). If the spawning prompt''s envelope arrives WITHOUT isSidechain==true (or without a parentUuid that roots at the Agent), the item is never added to ownedSidechainUuids, _visibleItems() (:116-160) never drops it, and _ConversationTurn.build() falls through past the ''when i.injected || i.isSidechain'' guard into the default UserMessage() case => label: ''you'' (the blue user card).
Need to confirm whether the prompt envelope is missing isSidechain (likely the spawning prompt is the FIRST user turn of the sidechain and may be emitted by the harness without the flag, or with a parentUuid that points at the spawning turn rather than the Agent tool_use). Check stream_json_session.dart emission too.
Fix direction (TBD after confirming the envelope shape): either (a) ensure the prompt envelope is correctly flagged/parented, or (b) in _sidechainFold associate the first sidechain-adjacent user prompt with its Agent tool_use even when the flag/chain is incomplete. Related: T-263 (prompt folds into the call), T-264 (run nests under the card).', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-10 13:47:04', '2026-06-10 13:47:04', NULL, '564de005ff82db2137a7fc173038dd46', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_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 (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB3JM0AXK1CTWD720RV1AVZ0', 'story', '06FB0TNQM5TWC00GW0P3X02HZW', 'Pick-up sets ticket to in_progress on accept', 'When a ticket is handed to Claude via the sidebar Pick-up button (T-327), also transition the ticket to in_progress — but on the receiving side of the message bus, not in the button.
Today the Pick-up button (`_PickUpAction` in lib/builtin/tickets/src/tickets_view.dart) only fetches the ticket and publishes a (builtin.tickets, pick-up) message carrying {id, prompt}. The Claude pane receives it in `_onTicketPickUp` (lib/builtin/claude/src/extension.dart:359) and injects the prompt into the active session. The state transition should live here, gated on acceptance i.e. only set in_progress once a live session actually accepts the prompt (injectMessage succeeds against a target session), so a pick-up with no live Claude pane stays a quiet no-op and does not mutate state.
Implementation notes:
- In `_onTicketPickUp`, after a successful injectMessage to a target session, call the existing status transition path: IPC `pql.tickets.status` with {ids: [id], status: ''in_progress''} (same call the detail view''s _StatusControls uses).
- The ''id'' is already in the message payload currently only ''prompt'' is read; also read ''id''.
- After a successful transition, publish (builtin.tickets, changed, {id}) so the sidebar/detail refresh, matching ticket_detail_view.dart behaviour.
- Keep it idempotent/safe: if the ticket is already in_progress (or terminal), don''t fight the user consider skipping when current status != open/todo. Verify the configured status vocabulary via `pql ticket statuslist`.
- No live session => no injection => no state change (preserve existing no-op semantics).
Acceptance:
- Picking up a ticket with a live Claude pane injects the prompt AND moves the ticket to in_progress, with the sidebar reflecting the new state.
- Picking up with no live session leaves the ticket untouched.
- Test coverage on _onTicketPickUp for both the accepted and no-session paths.', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-10 13:48:18', '2026-06-10 13:48:18', NULL, '33c0a8b0a26f1012dffc3dfa1e742463', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_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);