clickable http links in the Claude conversation; bundle markdown hooks (T-253)

http(s) links (typed or autolinked) in the conversation now open via the OS URL
handler (OsBridge.openURL) on click, with a hover underline + pointer; non-http
schemes stay inert. Works across prose, lists, tables, and headings.

Refactor: ClideMarkdown's growing set of inline-interaction callbacks
(onRecordTap, onImageToken, onLinkTap) is bundled into one ClideMarkdownHooks
value threaded as a single param — no more per-callback threading, and the hooks
now reach every context uniformly (links/images previously only worked in some).
The public widget API is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-09 22:16:57 +02:00
co-authored by Claude Opus 4.8
parent dc8ae0beaa
commit 1e8e37c6f9
4 changed files with 167 additions and 49 deletions
@@ -9,6 +9,7 @@
/// separate concern (T-138).
library;
import 'dart:async';
import 'dart:convert';
import 'dart:io';
@@ -360,6 +361,11 @@ void _openRecord(BuildContext context, String id) {
ClideKernel.of(context).messages.publish(publisher, 'selection', {'id': id});
}
/// Hand a clicked conversation link to the OS URL handler (T-253).
void _openUrl(BuildContext context, String url) {
unawaited(ClideKernel.of(context).os.openURL(url));
}
/// One conversation item, rendered by kind.
class _ConversationTurn extends StatelessWidget {
const _ConversationTurn({
@@ -421,6 +427,7 @@ class _ConversationTurn extends StatelessWidget {
i.text,
onRecordTap: (id) => _openRecord(context, id),
onImageToken: (path) => ImageThumbnail(path: path, size: 48),
onLinkTap: (url) => _openUrl(context, url),
),
),
// Sub-agent (sidechain) prose is NOT the main Claude — attribute it to the
@@ -430,7 +437,7 @@ class _ConversationTurn extends StatelessWidget {
accent: i.isSidechain ? tokens.globalTextMuted : claudeAccent,
label: i.isSidechain ? 'agent' : 'claude',
copyText: i.text,
body: ClideMarkdown(i.text, onRecordTap: (id) => _openRecord(context, id)),
body: ClideMarkdown(i.text, onRecordTap: (id) => _openRecord(context, id), onLinkTap: (url) => _openUrl(context, url)),
),
AssistantThinkingMessage() => ConversationCard(
variant: ConversationCardVariant.bare,