fix(fonts): honour the monospace setting in context-free render helpers (T-472)
Eleven leaf sites rendered monospace text from top-level/static helpers that take no BuildContext, so they hard-coded clideMonoFamily and ignored the Settings → Appearance Monospace choice that the D-101 facade made live everywhere else. Thread the resolved family in from the nearest context-bearing caller: - claude tool bodies/results: a required `mono` field on _ConversationTurn + a `mono` arg on the shared toolInputBody chain; - markdown inline `code`/record/file-ref spans: carried on ClideMarkdownHooks, which build() already constructs from context and threads to every static; - search preview styles and welcome tips: a `mono` parameter on the helpers. No behaviour change when the setting is default; these surfaces now switch live with the rest. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5923,3 +5923,7 @@ 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 ('06FD91A7VEW3VCY7QX1END2Z3G', 'status', 'in_progress', 'done', NULL, '2026-06-17 18:14:38', '2026-06-17 18:14:38.272', '2026-06-17 18:14:38.272', NULL, '9c7cb301a7539212e90bf16210684d97', 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 ('06FDDJ17C3GZWNE98NRVXP189C', 'status', 'backlog', 'in_progress', NULL, '2026-06-17 18:18:13', '2026-06-17 18:18:13.607', '2026-06-17 18:18:13.607', NULL, 'b8f8460933a4b9393acc816c167d725d', 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 ('06FDDJ17C3GZWNE98NRVXP189C', 'status', 'in_progress', 'done', NULL, '2026-06-17 18:59:09', '2026-06-17 18:59:09.679', '2026-06-17 18:59:09.679', NULL, 'a088e76684e5d0c4922c62082802dd2d', 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 ('06FDDTCGG5Z0KNQKF89W1VZSQ8', 'description', NULL, 'Per D-101. The 11 sites that need a BuildContext threaded from the nearest context-bearing caller (resolve ClideSettings.fonts.monoOf(context) and pass the family in): prompt_card.dart toolEditBody/toolReadLikeBody/toolPathLine (top-level fns); conversation_view.dart _toolResult; welcome_view.dart _tipRow; search_panel_view.dart _base getter; clide_markdown.dart _inlineElementSpan/_recordLinkSpan/_fileLinkSpan (static span chain). Until done, these render the default JetBrains Mono regardless of the mono-font setting.', NULL, '2026-06-17 19:03:19', '2026-06-17 19:03:19.250', '2026-06-17 19:03:19.250', NULL, 'dcdd0348a9cd88a23ac1b3711d554c58', 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 ('06FDDX5GVH3FTCVDEC1QAFACY4', 'description', NULL, 'Per D-101. Migrate ClideTheme.of(context) consumers (hundreds of sites) to ClideSettings.theme.of(context). The facade already delegates to ClideTheme, so this is a mechanical, no-behavior-change sweep — do it in focused batches with analyze between, ideally via a scripted find/replace + import fixups.', NULL, '2026-06-17 19:03:21', '2026-06-17 19:03:21.095', '2026-06-17 19:03:21.095', NULL, '45e0fb561bb187552a423d098aeb1cb5', 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 ('06FDDX5KEAJ8KRWVV7ZSG6H7A0', 'description', NULL, 'Per D-101. Migrate i18n reads (ClideKernel.of(context).i18n[.string]) to ClideSettings.i18n.of(context). The facade delegates to the same I18n service, so behaviour is unchanged; mechanical sweep in batches.', NULL, '2026-06-17 19:03:22', '2026-06-17 19:03:22.235', '2026-06-17 19:03:22.235', NULL, '5d617ff30e22d3b4c57ca6325920d91a', 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 ('06FDDTCGG5Z0KNQKF89W1VZSQ8', 'status', 'backlog', 'done', NULL, '2026-06-17 19:35:22', '2026-06-17 19:35:22.690', '2026-06-17 19:35:22.690', NULL, '580448456994d6acecc88cfbd40a595f', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
|
||||
@@ -7242,3 +7242,7 @@ INSERT INTO tickets (record_id, type, parent_record_id, title, description, stat
|
||||
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 ('06FDDJ17C3GZWNE98NRVXP189C', 'task', '06FD07ECS3GK2V7Z2WYYPJHJYC', 'Settings → Appearance: monospace font picker (needs context-driven mono family)', 'Follow-up to T-460. T-460 shipped the UI-font picker (live via the root DefaultTextStyle, single seam) + bundled Inter as default. The MONOSPACE picker is deferred because clideMonoFamily is referenced as a const in ~38 sites (terminal, diff, code blocks, ClideText fontFamily) — not inherited. To make the mono picker apply live, route the mono family through context (a small FontScope InheritedWidget or the theme) read by those sites, then add a ''Monospace font'' select in Appearance writing app.mono.font (kMonoFontSettingKey already defined in typography.dart). Default JetBrains Mono.', 'done', 'medium', NULL, NULL, NULL, '2026-06-17 18:11:42.049', '2026-06-17 18:59:09.679', NULL, 'e81367cebfa243d563325587539f0a3c', 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 ('06FDDX5GVH3FTCVDEC1QAFACY4', 'task', '06FD07ECS3GK2V7Z2WYYPJHJYC', 'Migrate theme consumers (ClideTheme.of → ClideSettings.theme.of) per D-101', NULL, 'backlog', 'medium', NULL, NULL, NULL, '2026-06-17 19:00:20.828', '2026-06-17 19:00:20.828', NULL, '0a090c8cf04b03de3b82f2e6ea3029de', 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 ('06FDDX5KEAJ8KRWVV7ZSG6H7A0', 'task', '06FD07ECS3GK2V7Z2WYYPJHJYC', 'Migrate i18n consumers (ClideKernel.i18n → ClideSettings.i18n.of) per D-101', NULL, 'backlog', 'medium', NULL, NULL, NULL, '2026-06-17 19:00:21.490', '2026-06-17 19:00:21.490', NULL, '273598190d1cafe66e693899852916e7', 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 ('06FDDTCGG5Z0KNQKF89W1VZSQ8', 'task', '06FD07ECS3GK2V7Z2WYYPJHJYC', 'Migrate 11 context-less mono-font sites onto ClideSettings.fonts (thread family through helpers)', 'Per D-101. The 11 sites that need a BuildContext threaded from the nearest context-bearing caller (resolve ClideSettings.fonts.monoOf(context) and pass the family in): prompt_card.dart toolEditBody/toolReadLikeBody/toolPathLine (top-level fns); conversation_view.dart _toolResult; welcome_view.dart _tipRow; search_panel_view.dart _base getter; clide_markdown.dart _inlineElementSpan/_recordLinkSpan/_fileLinkSpan (static span chain). Until done, these render the default JetBrains Mono regardless of the mono-font setting.', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-17 18:48:11.649', '2026-06-17 19:03:19.250', NULL, 'fb570bedf09fd60eb835f107310766d4', 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 ('06FDDX5GVH3FTCVDEC1QAFACY4', 'task', '06FD07ECS3GK2V7Z2WYYPJHJYC', 'Migrate theme consumers (ClideTheme.of → ClideSettings.theme.of) per D-101', 'Per D-101. Migrate ClideTheme.of(context) consumers (hundreds of sites) to ClideSettings.theme.of(context). The facade already delegates to ClideTheme, so this is a mechanical, no-behavior-change sweep — do it in focused batches with analyze between, ideally via a scripted find/replace + import fixups.', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-17 19:00:20.828', '2026-06-17 19:03:21.095', NULL, 'b9144ecc303e9223fe212b17ee79c9d8', 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 ('06FDDX5KEAJ8KRWVV7ZSG6H7A0', 'task', '06FD07ECS3GK2V7Z2WYYPJHJYC', 'Migrate i18n consumers (ClideKernel.i18n → ClideSettings.i18n.of) per D-101', 'Per D-101. Migrate i18n reads (ClideKernel.of(context).i18n[.string]) to ClideSettings.i18n.of(context). The facade delegates to the same I18n service, so behaviour is unchanged; mechanical sweep in batches.', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-17 19:00:21.490', '2026-06-17 19:03:22.235', NULL, '16d6229039400bd826a77a08eca9573b', 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 ('06FDDTCGG5Z0KNQKF89W1VZSQ8', 'task', '06FD07ECS3GK2V7Z2WYYPJHJYC', 'Migrate 11 context-less mono-font sites onto ClideSettings.fonts (thread family through helpers)', 'Per D-101. The 11 sites that need a BuildContext threaded from the nearest context-bearing caller (resolve ClideSettings.fonts.monoOf(context) and pass the family in): prompt_card.dart toolEditBody/toolReadLikeBody/toolPathLine (top-level fns); conversation_view.dart _toolResult; welcome_view.dart _tipRow; search_panel_view.dart _base getter; clide_markdown.dart _inlineElementSpan/_recordLinkSpan/_fileLinkSpan (static span chain). Until done, these render the default JetBrains Mono regardless of the mono-font setting.', 'done', 'medium', NULL, NULL, NULL, '2026-06-17 18:48:11.649', '2026-06-17 19:35:22.690', NULL, '7dd6dae4197fc95d0bc6becfad4eecbb', 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);
|
||||
|
||||
@@ -79,6 +79,13 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit.
|
||||
changed from "Settings…" to "Theme…" so it no longer collides with the new
|
||||
Settings panel in the palette; behaviour is unchanged. (T-445)
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Monospace setting now applies everywhere.** Eleven context-free render
|
||||
helpers (Claude tool bodies + results, inline markdown code/refs, search
|
||||
previews, welcome tips) hard-coded JetBrains Mono and ignored the Monospace
|
||||
font setting; they now honour it live. (T-472)
|
||||
|
||||
## [2.6.0] — 2026-06-16
|
||||
|
||||
### Added
|
||||
|
||||
@@ -336,6 +336,7 @@ class _ConversationViewState extends State<ConversationView> {
|
||||
key: ValueKey('turn.${item.uuid}'),
|
||||
item: item,
|
||||
tokens: tokens,
|
||||
mono: ClideSettings.fonts.monoOf(context),
|
||||
collapseTools: true,
|
||||
toolUseOutcomes: widget.toolUseOutcomes,
|
||||
quietErrorToolUseIds: widget.quietErrorToolUseIds,
|
||||
@@ -547,6 +548,7 @@ class _ConversationTurn extends StatelessWidget {
|
||||
super.key,
|
||||
required this.item,
|
||||
required this.tokens,
|
||||
required this.mono,
|
||||
this.collapseTools = false,
|
||||
this.toolUseOutcomes = const <String, bool>{},
|
||||
this.quietErrorToolUseIds = const <String>{},
|
||||
@@ -560,6 +562,11 @@ class _ConversationTurn extends StatelessWidget {
|
||||
final ConversationItem item;
|
||||
final SurfaceTokens tokens;
|
||||
|
||||
/// The live monospace family (T-471/T-472), resolved from context by the
|
||||
/// parent and threaded in so the context-free tool-body/result helpers honour
|
||||
/// the Settings → Appearance choice.
|
||||
final String mono;
|
||||
|
||||
/// When true (top-level stream items), a tool use renders as its own
|
||||
/// collapser over a one-item list (T-305). When false (already inside a run /
|
||||
/// edit collapser), it renders the bare inner content card so collapsers
|
||||
@@ -788,6 +795,7 @@ class _ConversationTurn extends StatelessWidget {
|
||||
key: ValueKey('run.${r.uuid}'),
|
||||
item: r,
|
||||
tokens: tokens,
|
||||
mono: mono,
|
||||
toolUseOutcomes: toolUseOutcomes,
|
||||
quietErrorToolUseIds: quietErrorToolUseIds,
|
||||
toolUseById: toolUseById,
|
||||
@@ -948,7 +956,7 @@ class _ConversationTurn extends StatelessWidget {
|
||||
label: t.name,
|
||||
copyText: const JsonEncoder.withIndent(' ').convert(t.input),
|
||||
status: status,
|
||||
body: toolInputBody(tokens, t.name, t.input),
|
||||
body: toolInputBody(tokens, t.name, t.input, mono),
|
||||
extraSegments: segments,
|
||||
// Inside a collapser the surrounding padding is even on all sides
|
||||
// (T-305): a matching bottom margin is the canvas's bottom inset and the
|
||||
@@ -1005,7 +1013,7 @@ class _ConversationTurn extends StatelessWidget {
|
||||
collapsible: quiet || multiline,
|
||||
collapsedByDefault: quiet, // genuine errors stay expanded; a denial folds
|
||||
collapsedSummary: (quiet || multiline) ? _firstLine(t.content) : null,
|
||||
body: ClideText(t.content, fontSize: clideFontMeta, fontFamily: clideMonoFamily, color: quiet ? tokens.globalTextMuted : tokens.statusError),
|
||||
body: ClideText(t.content, fontSize: clideFontMeta, fontFamily: mono, color: quiet ? tokens.globalTextMuted : tokens.statusError),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1026,7 +1034,7 @@ class _ConversationTurn extends StatelessWidget {
|
||||
collapsedSummary: multiline ? _firstLine(t.content) : null,
|
||||
body: isOutputTool
|
||||
? ClideCodeBlock(source: t.content, language: 'text')
|
||||
: ClideText(t.content, fontSize: clideFontMeta, fontFamily: clideMonoFamily, color: tokens.globalForeground),
|
||||
: ClideText(t.content, fontSize: clideFontMeta, fontFamily: mono, color: tokens.globalForeground),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1089,6 +1097,7 @@ class _ActivityCard extends StatelessWidget {
|
||||
key: ValueKey('step.${item.uuid}'),
|
||||
item: item,
|
||||
tokens: tokens,
|
||||
mono: ClideSettings.fonts.monoOf(context),
|
||||
toolUseOutcomes: toolUseOutcomes,
|
||||
quietErrorToolUseIds: quietErrorToolUseIds,
|
||||
toolUseById: toolUseById,
|
||||
@@ -1154,6 +1163,7 @@ class _EditRunCard extends StatelessWidget {
|
||||
key: ValueKey('edit.${item.uuid}'),
|
||||
item: item,
|
||||
tokens: tokens,
|
||||
mono: ClideSettings.fonts.monoOf(context),
|
||||
toolUseOutcomes: toolUseOutcomes,
|
||||
quietErrorToolUseIds: quietErrorToolUseIds,
|
||||
toolUseById: toolUseById,
|
||||
|
||||
@@ -259,7 +259,7 @@ class _ToolPromptCardState extends State<ToolPromptCard> {
|
||||
// or file body is visible but doesn't swamp the composer zone (D-78).
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxHeight: 200),
|
||||
child: SingleChildScrollView(child: _inputBody(tokens, p)),
|
||||
child: SingleChildScrollView(child: _inputBody(tokens, ClideSettings.fonts.monoOf(context), p)),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Wrap(
|
||||
@@ -284,7 +284,7 @@ class _ToolPromptCardState extends State<ToolPromptCard> {
|
||||
|
||||
/// Render the tool input in the shape that best fits the tool. Delegates to
|
||||
/// the shared top-level helpers (also used by ConversationView — T-168).
|
||||
Widget _inputBody(SurfaceTokens tokens, ToolPrompt p) => toolInputBody(tokens, p.toolName, p.input);
|
||||
Widget _inputBody(SurfaceTokens tokens, String mono, ToolPrompt p) => toolInputBody(tokens, p.toolName, p.input, mono);
|
||||
|
||||
// -- AskUserQuestion: single = bare, multi = stepper + review --------------
|
||||
|
||||
@@ -492,19 +492,19 @@ class _ToolPromptCardState extends State<ToolPromptCard> {
|
||||
///
|
||||
/// Shared between [ToolPromptCard] (permission prompt body) and the
|
||||
/// [ConversationView] tool-use card bodies (T-168).
|
||||
Widget toolInputBody(SurfaceTokens tokens, String toolName, Map<String, dynamic> input) {
|
||||
Widget toolInputBody(SurfaceTokens tokens, String toolName, Map<String, dynamic> input, String mono) {
|
||||
switch (toolName) {
|
||||
case 'Bash':
|
||||
return toolBashBody(tokens, input);
|
||||
case 'Write':
|
||||
return toolWriteBody(tokens, input);
|
||||
return toolWriteBody(tokens, input, mono);
|
||||
case 'Edit':
|
||||
case 'MultiEdit':
|
||||
return toolEditBody(tokens, input);
|
||||
return toolEditBody(tokens, input, mono);
|
||||
case 'Read':
|
||||
case 'Grep':
|
||||
case 'LS':
|
||||
return toolReadLikeBody(tokens, toolName, input);
|
||||
return toolReadLikeBody(tokens, toolName, input, mono);
|
||||
default:
|
||||
return ClideCodeBlock(source: const JsonEncoder.withIndent(' ').convert(input), language: 'json');
|
||||
}
|
||||
@@ -530,21 +530,21 @@ Widget toolBashBody(SurfaceTokens tokens, Map<String, dynamic> input) {
|
||||
}
|
||||
|
||||
/// Write tool body: the file path + content with syntax highlighting.
|
||||
Widget toolWriteBody(SurfaceTokens tokens, Map<String, dynamic> input) {
|
||||
Widget toolWriteBody(SurfaceTokens tokens, Map<String, dynamic> input, String mono) {
|
||||
final path = input['file_path'] as String? ?? '';
|
||||
final content = input['content'] as String? ?? '';
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (path.isNotEmpty) toolPathLine(tokens, path),
|
||||
if (path.isNotEmpty) toolPathLine(tokens, path, mono),
|
||||
ClideCodeBlock(source: content, language: grammarForPath(path)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Edit / MultiEdit tool body: before/after diff view.
|
||||
Widget toolEditBody(SurfaceTokens tokens, Map<String, dynamic> input) {
|
||||
Widget toolEditBody(SurfaceTokens tokens, Map<String, dynamic> input, String mono) {
|
||||
final path = input['file_path'] as String? ?? '';
|
||||
final oldStr = input['old_string'] as String? ?? '';
|
||||
final newStr = input['new_string'] as String? ?? '';
|
||||
@@ -553,12 +553,12 @@ Widget toolEditBody(SurfaceTokens tokens, Map<String, dynamic> input) {
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (path.isNotEmpty) toolPathLine(tokens, path),
|
||||
ClideText('— before', fontSize: clideFontMeta, color: tokens.globalTextMuted, fontFamily: clideMonoFamily),
|
||||
if (path.isNotEmpty) toolPathLine(tokens, path, mono),
|
||||
ClideText('— before', fontSize: clideFontMeta, color: tokens.globalTextMuted, fontFamily: mono),
|
||||
const SizedBox(height: 4),
|
||||
ClideCodeBlock(source: oldStr, language: lang),
|
||||
const SizedBox(height: 8),
|
||||
ClideText('+ after', fontSize: clideFontMeta, color: tokens.globalTextMuted, fontFamily: clideMonoFamily),
|
||||
ClideText('+ after', fontSize: clideFontMeta, color: tokens.globalTextMuted, fontFamily: mono),
|
||||
const SizedBox(height: 4),
|
||||
ClideCodeBlock(source: newStr, language: lang),
|
||||
],
|
||||
@@ -568,7 +568,7 @@ Widget toolEditBody(SurfaceTokens tokens, Map<String, dynamic> input) {
|
||||
/// Read / Grep / LS body: show the file path or pattern as a one-liner label
|
||||
/// so the card stays compact. These tools produce the interesting output in the
|
||||
/// result card rather than their input.
|
||||
Widget toolReadLikeBody(SurfaceTokens tokens, String toolName, Map<String, dynamic> input) {
|
||||
Widget toolReadLikeBody(SurfaceTokens tokens, String toolName, Map<String, dynamic> input, String mono) {
|
||||
final path = input['file_path'] ?? input['path'] ?? input['pattern'] ?? '';
|
||||
final extra = <String>[];
|
||||
if (toolName == 'Grep') {
|
||||
@@ -576,13 +576,13 @@ Widget toolReadLikeBody(SurfaceTokens tokens, String toolName, Map<String, dynam
|
||||
if (pat != null && pat.isNotEmpty) extra.add('"$pat"');
|
||||
}
|
||||
final label = [path.toString(), ...extra].where((s) => s.isNotEmpty).join(' ');
|
||||
return ClideText(label.isNotEmpty ? label : toolName, fontSize: clideFontMeta, fontFamily: clideMonoFamily, color: tokens.globalForeground);
|
||||
return ClideText(label.isNotEmpty ? label : toolName, fontSize: clideFontMeta, fontFamily: mono, color: tokens.globalForeground);
|
||||
}
|
||||
|
||||
/// A muted file path line, shared across tool bodies.
|
||||
Widget toolPathLine(SurfaceTokens tokens, String path) => Padding(
|
||||
Widget toolPathLine(SurfaceTokens tokens, String path, String mono) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 6),
|
||||
child: ClideText(path, fontSize: clideFontMeta, fontFamily: clideMonoFamily, color: tokens.globalTextMuted),
|
||||
child: ClideText(path, fontSize: clideFontMeta, fontFamily: mono, color: tokens.globalTextMuted),
|
||||
);
|
||||
|
||||
// -- shared note / free-text field -------------------------------------------
|
||||
|
||||
@@ -346,7 +346,7 @@ class _MatchRow extends StatelessWidget {
|
||||
width: 36,
|
||||
child: ClideText('${match.line}', fontSize: clideFontCaption, fontFamily: ClideSettings.fonts.monoOf(context), color: tokens.globalTextMuted),
|
||||
),
|
||||
Expanded(child: replacement.isEmpty ? _highlighted() : _preview()),
|
||||
Expanded(child: replacement.isEmpty ? _highlighted(ClideSettings.fonts.monoOf(context)) : _preview(ClideSettings.fonts.monoOf(context))),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -354,9 +354,9 @@ class _MatchRow extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
TextStyle get _base => TextStyle(fontFamily: clideMonoFamily, fontSize: clideFontCaption, color: tokens.sidebarForeground);
|
||||
TextStyle _base(String mono) => TextStyle(fontFamily: mono, fontSize: clideFontCaption, color: tokens.sidebarForeground);
|
||||
|
||||
Widget _highlighted() {
|
||||
Widget _highlighted(String mono) {
|
||||
final line = match.preview;
|
||||
final start = match.matchStart.clamp(0, line.length);
|
||||
final end = match.matchEnd.clamp(start, line.length);
|
||||
@@ -364,12 +364,12 @@ class _MatchRow extends StatelessWidget {
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
text: TextSpan(
|
||||
style: _base,
|
||||
style: _base(mono),
|
||||
children: [
|
||||
TextSpan(text: line.substring(0, start)),
|
||||
TextSpan(
|
||||
text: line.substring(start, end),
|
||||
style: _base.copyWith(color: tokens.globalFocus, fontWeight: FontWeight.bold),
|
||||
style: _base(mono).copyWith(color: tokens.globalFocus, fontWeight: FontWeight.bold),
|
||||
),
|
||||
TextSpan(text: line.substring(end)),
|
||||
],
|
||||
@@ -379,7 +379,7 @@ class _MatchRow extends StatelessWidget {
|
||||
|
||||
/// Replace-preview: the original line struck through, then the
|
||||
/// rewritten line (computed with the same engine the apply uses).
|
||||
Widget _preview() {
|
||||
Widget _preview(String mono) {
|
||||
final after = applyToText(match.preview, query, replacement).text;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -389,7 +389,7 @@ class _MatchRow extends StatelessWidget {
|
||||
overflow: TextOverflow.ellipsis,
|
||||
text: TextSpan(
|
||||
text: match.preview,
|
||||
style: _base.copyWith(decoration: TextDecoration.lineThrough, color: tokens.globalTextMuted),
|
||||
style: _base(mono).copyWith(decoration: TextDecoration.lineThrough, color: tokens.globalTextMuted),
|
||||
),
|
||||
),
|
||||
RichText(
|
||||
@@ -397,7 +397,7 @@ class _MatchRow extends StatelessWidget {
|
||||
overflow: TextOverflow.ellipsis,
|
||||
text: TextSpan(
|
||||
text: after,
|
||||
style: _base.copyWith(color: tokens.globalFocus),
|
||||
style: _base(mono).copyWith(color: tokens.globalFocus),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -107,16 +107,16 @@ class _TipsCard extends StatelessWidget {
|
||||
children: [
|
||||
ClideText('TIPS', fontSize: clideFontSmall, color: tokens.sidebarSectionHeader, fontFamily: ClideSettings.fonts.monoOf(context)),
|
||||
const SizedBox(height: 14),
|
||||
_tipRow(firstRow),
|
||||
_tipRow(firstRow, ClideSettings.fonts.monoOf(context)),
|
||||
const SizedBox(height: 8),
|
||||
_tipRow(secondRow),
|
||||
_tipRow(secondRow, ClideSettings.fonts.monoOf(context)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _tipRow(List<(String, String)> tips) {
|
||||
Widget _tipRow(List<(String, String)> tips, String mono) {
|
||||
return Row(
|
||||
children: [
|
||||
for (var i = 0; i < tips.length; i++) ...[
|
||||
@@ -126,7 +126,7 @@ class _TipsCard extends StatelessWidget {
|
||||
Expanded(
|
||||
child: ClideText(tips[i].$1, fontSize: clideFontMeta, color: tokens.globalTextMuted),
|
||||
),
|
||||
ClideText(tips[i].$2, fontSize: clideFontSmall, color: tokens.globalForeground, fontFamily: clideMonoFamily),
|
||||
ClideText(tips[i].$2, fontSize: clideFontSmall, color: tokens.globalForeground, fontFamily: mono),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:clide/kernel/src/theme/controller.dart';
|
||||
import 'package:clide/kernel/src/theme/tokens.dart';
|
||||
import 'package:clide/widgets/src/clide_code_block.dart';
|
||||
import 'package:clide/widgets/src/clide_divider.dart';
|
||||
import 'package:clide/widgets/src/clide_settings.dart';
|
||||
import 'package:clide/widgets/src/clide_text.dart';
|
||||
import 'package:clide/widgets/src/clide_tappable.dart';
|
||||
import 'package:clide/widgets/src/typography.dart';
|
||||
@@ -35,7 +36,7 @@ typedef FileTapCallback = void Function(String path, int? line);
|
||||
/// inert (the text renders, just not interactive).
|
||||
@immutable
|
||||
class ClideMarkdownHooks {
|
||||
const ClideMarkdownHooks({this.onRecordTap, this.onImageToken, this.onLinkTap, this.resolveFileRef, this.onOpenFile});
|
||||
const ClideMarkdownHooks({this.onRecordTap, this.onImageToken, this.onLinkTap, this.resolveFileRef, this.onOpenFile, this.mono = clideMonoFamily});
|
||||
|
||||
/// Tap a governance/ticket ref (T-281, D-77, …) → open the record (T-279).
|
||||
final RecordTapCallback? onRecordTap;
|
||||
@@ -53,6 +54,12 @@ class ClideMarkdownHooks {
|
||||
/// Open a resolved workspace file in the editor (T-300).
|
||||
final FileTapCallback? onOpenFile;
|
||||
|
||||
/// The live monospace family (Settings → Appearance, T-471). Resolved once
|
||||
/// from context at the widget entry and threaded through the static span
|
||||
/// chain via this carrier, so inline `code` and ref spans honour the setting
|
||||
/// even though the span builders are context-free statics (T-472).
|
||||
final String mono;
|
||||
|
||||
static const none = ClideMarkdownHooks();
|
||||
}
|
||||
|
||||
@@ -116,6 +123,7 @@ class ClideMarkdown extends StatelessWidget {
|
||||
onLinkTap: onLinkTap,
|
||||
resolveFileRef: resolveFileRef,
|
||||
onOpenFile: onOpenFile,
|
||||
mono: ClideSettings.fonts.monoOf(context),
|
||||
);
|
||||
final widgets = _buildNodes(nodes, tokens, hooks);
|
||||
return Column(crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, children: widgets);
|
||||
@@ -372,16 +380,16 @@ class ClideMarkdown extends StatelessWidget {
|
||||
// editor (T-300); other inline code renders verbatim.
|
||||
if (hooks.resolveFileRef != null && hooks.onOpenFile != null) {
|
||||
final ref = _codeFileRef(raw, hooks.resolveFileRef!);
|
||||
if (ref != null) return _fileLinkSpan(raw, ref.$1, ref.$2, tokens, hooks.onOpenFile!, mono: true);
|
||||
if (ref != null) return _fileLinkSpan(raw, ref.$1, ref.$2, tokens, hooks.onOpenFile!, hooks.mono, mono: true);
|
||||
}
|
||||
return TextSpan(
|
||||
text: raw,
|
||||
style: TextStyle(fontFamily: clideMonoFamily, fontSize: clideFontMono, color: tokens.syntaxString, backgroundColor: tokens.panelBackground),
|
||||
style: TextStyle(fontFamily: hooks.mono, fontSize: clideFontMono, color: tokens.syntaxString, backgroundColor: tokens.panelBackground),
|
||||
);
|
||||
case 'a':
|
||||
final text = _unescapeHtml(el.textContent);
|
||||
if (hooks.onRecordTap != null && _recordPattern.hasMatch(text)) {
|
||||
return _recordLinkSpan(text, tokens, hooks.onRecordTap!);
|
||||
return _recordLinkSpan(text, tokens, hooks.onRecordTap!, hooks.mono);
|
||||
}
|
||||
// An http(s) link (explicit or autolinked) → tappable, opens via the
|
||||
// caller's handler (T-253). Non-http schemes stay coloured-but-inert.
|
||||
@@ -394,7 +402,7 @@ class ClideMarkdown extends StatelessWidget {
|
||||
if (hooks.resolveFileRef != null && hooks.onOpenFile != null && href != null) {
|
||||
final (path, line) = _splitFileRef(href);
|
||||
final abs = hooks.resolveFileRef!(path);
|
||||
if (abs != null) return _fileLinkSpan(text, abs, line, tokens, hooks.onOpenFile!);
|
||||
if (abs != null) return _fileLinkSpan(text, abs, line, tokens, hooks.onOpenFile!, hooks.mono);
|
||||
}
|
||||
return TextSpan(
|
||||
text: text,
|
||||
@@ -467,7 +475,7 @@ class ClideMarkdown extends StatelessWidget {
|
||||
final hits = <_LinkHit>[];
|
||||
if (wantRecords) {
|
||||
for (final m in _bareRecordPattern.allMatches(text)) {
|
||||
hits.add(_LinkHit(m.start, m.end, _recordLinkSpan(m[0]!, tokens, hooks.onRecordTap!)));
|
||||
hits.add(_LinkHit(m.start, m.end, _recordLinkSpan(m[0]!, tokens, hooks.onRecordTap!, hooks.mono)));
|
||||
}
|
||||
}
|
||||
if (wantFiles) {
|
||||
@@ -475,7 +483,7 @@ class ClideMarkdown extends StatelessWidget {
|
||||
final abs = hooks.resolveFileRef!(m.group(1)!);
|
||||
if (abs == null) continue;
|
||||
final line = m.group(2) == null ? null : int.tryParse(m.group(2)!);
|
||||
hits.add(_LinkHit(m.start, m.end, _fileLinkSpan(text.substring(m.start, m.end), abs, line, tokens, hooks.onOpenFile!)));
|
||||
hits.add(_LinkHit(m.start, m.end, _fileLinkSpan(text.substring(m.start, m.end), abs, line, tokens, hooks.onOpenFile!, hooks.mono)));
|
||||
}
|
||||
}
|
||||
if (hits.isEmpty) return [TextSpan(text: text)];
|
||||
@@ -517,7 +525,7 @@ class ClideMarkdown extends StatelessWidget {
|
||||
/// A clickable record-reference span: [id] rendered in the focus accent with
|
||||
/// a hover underline, firing [onRecordTap] on tap (T-279). Shared by bare-text
|
||||
/// refs and record-shaped markdown links so both look and behave alike.
|
||||
static InlineSpan _recordLinkSpan(String id, SurfaceTokens tokens, RecordTapCallback onRecordTap) {
|
||||
static InlineSpan _recordLinkSpan(String id, SurfaceTokens tokens, RecordTapCallback onRecordTap, String mono) {
|
||||
return WidgetSpan(
|
||||
alignment: PlaceholderAlignment.baseline,
|
||||
baseline: TextBaseline.alphabetic,
|
||||
@@ -529,7 +537,7 @@ class ClideMarkdown extends StatelessWidget {
|
||||
color: tokens.globalFocus,
|
||||
fontSize: _fontSize,
|
||||
height: _lineHeight,
|
||||
fontFamily: clideMonoFamily,
|
||||
fontFamily: mono,
|
||||
decoration: hovered ? TextDecoration.underline : null,
|
||||
decorationColor: tokens.globalFocus,
|
||||
),
|
||||
@@ -547,7 +555,15 @@ class ClideMarkdown extends StatelessWidget {
|
||||
/// the focus accent, underlined on hover, opening the resolved [absPath] at
|
||||
/// [line] (when present) in the editor via [onOpenFile]. The [mono] flag keeps
|
||||
/// backticked refs in the monospace face; prose refs use the UI face.
|
||||
static InlineSpan _fileLinkSpan(String display, String absPath, int? line, SurfaceTokens tokens, FileTapCallback onOpenFile, {bool mono = false}) {
|
||||
static InlineSpan _fileLinkSpan(
|
||||
String display,
|
||||
String absPath,
|
||||
int? line,
|
||||
SurfaceTokens tokens,
|
||||
FileTapCallback onOpenFile,
|
||||
String monoFamily, {
|
||||
bool mono = false,
|
||||
}) {
|
||||
return WidgetSpan(
|
||||
alignment: PlaceholderAlignment.baseline,
|
||||
baseline: TextBaseline.alphabetic,
|
||||
@@ -560,7 +576,7 @@ class ClideMarkdown extends StatelessWidget {
|
||||
color: tokens.globalFocus,
|
||||
fontSize: mono ? clideFontMono : _fontSize,
|
||||
height: _lineHeight,
|
||||
fontFamily: mono ? clideMonoFamily : clideUiFamily,
|
||||
fontFamily: mono ? monoFamily : clideUiFamily,
|
||||
fontFamilyFallback: mono ? null : clideUiFamilyFallback,
|
||||
decoration: hovered ? TextDecoration.underline : null,
|
||||
decorationColor: tokens.globalFocus,
|
||||
|
||||
Reference in New Issue
Block a user