clickable T/D/Q/R cross-refs in the Claude conversation (T-279)

Linkify bare ticket/governance refs (T-281, D-77, Q-5, R-2) in rendered
conversation messages so clicking one opens the record in its context-pane
reader — T- in the tickets reader, D/Q/R in the decisions reader — reusing
the existing `selection` MessageBus addressing (the same path clide ui open
and the panels use; D-6 parity already satisfied by `clide ui open`).

ClideMarkdown now linkifies bare refs in running text (paragraphs, lists,
headings, bold/italic), not just record-shaped markdown links. Matching is
word-boundary anchored so "T-shirt" (no digits) and "PT-281" (mid-word) stay
literal; `code` spans and `pre` blocks render verbatim and never reach the
linkifier, so refs inside code stay plain. The clickable span is shared
between bare refs and record-shaped links so both look and behave alike.

Tests: ClideMarkdown linkifier cases (bare T/D/Q/R tap fires onRecordTap,
T-shirt + inline-code refs stay plain, no-callback stays plain); conversation
view integration (clicking a bare ref publishes the reader-open selection to
the tickets/decisions reader).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-08 16:13:01 +02:00
co-authored by Claude Opus 4.8
parent 12032e8c2c
commit 5241876871
7 changed files with 225 additions and 24 deletions
+11 -2
View File
@@ -298,6 +298,15 @@ const claudeAccent = Color(0xFFD97757);
/// `Task`; the Agent SDK surface uses `Agent` — accept both (T-263).
bool _isAgentTool(String name) => name == 'Task' || name == 'Agent';
/// Open a governance/ticket record clicked in the conversation (T-279) in its
/// context-pane reader, reusing the existing `selection` MessageBus addressing
/// (the same path `clide ui open` and the panels use, T-231/T-233): T-NNN → the
/// tickets reader, D/Q/R-NNN → the decisions reader.
void _openRecord(BuildContext context, String id) {
final publisher = id.startsWith('T-') ? 'builtin.tickets' : 'builtin.decisions';
ClideKernel.of(context).messages.publish(publisher, 'selection', {'id': id});
}
/// One conversation item, rendered by kind.
class _ConversationTurn extends StatelessWidget {
const _ConversationTurn({
@@ -352,7 +361,7 @@ class _ConversationTurn extends StatelessWidget {
accent: tokens.globalFocus,
label: 'you',
copyText: i.text,
body: ClideMarkdown(i.text),
body: ClideMarkdown(i.text, onRecordTap: (id) => _openRecord(context, id)),
),
// 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
@@ -361,7 +370,7 @@ class _ConversationTurn extends StatelessWidget {
accent: i.isSidechain ? tokens.globalTextMuted : claudeAccent,
label: i.isSidechain ? 'agent' : 'claude',
copyText: i.text,
body: ClideMarkdown(i.text),
body: ClideMarkdown(i.text, onRecordTap: (id) => _openRecord(context, id)),
),
AssistantThinkingMessage() => ConversationCard(
variant: ConversationCardVariant.bare,