diff --git a/.pql/changelog/ticket_history/2026-06.sql b/.pql/changelog/ticket_history/2026-06.sql index 81af4ed6..643e2214 100644 --- a/.pql/changelog/ticket_history/2026-06.sql +++ b/.pql/changelog/ticket_history/2026-06.sql @@ -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; diff --git a/.pql/changelog/tickets/2026-06.sql b/.pql/changelog/tickets/2026-06.sql index ba3d4a0d..b95a9b6d 100644 --- a/.pql/changelog/tickets/2026-06.sql +++ b/.pql/changelog/tickets/2026-06.sql @@ -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); diff --git a/CHANGELOG.md b/CHANGELOG.md index 248abbe7..09352ce8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/lib/builtin/claude/src/conversation_view.dart b/lib/builtin/claude/src/conversation_view.dart index a1e7e92c..4d87e77d 100644 --- a/lib/builtin/claude/src/conversation_view.dart +++ b/lib/builtin/claude/src/conversation_view.dart @@ -336,6 +336,7 @@ class _ConversationViewState extends State { 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 {}, this.quietErrorToolUseIds = const {}, @@ -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, diff --git a/lib/builtin/claude/src/prompt_card.dart b/lib/builtin/claude/src/prompt_card.dart index cff35a72..fea7f750 100644 --- a/lib/builtin/claude/src/prompt_card.dart +++ b/lib/builtin/claude/src/prompt_card.dart @@ -259,7 +259,7 @@ class _ToolPromptCardState extends State { // 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 { /// 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 { /// /// Shared between [ToolPromptCard] (permission prompt body) and the /// [ConversationView] tool-use card bodies (T-168). -Widget toolInputBody(SurfaceTokens tokens, String toolName, Map input) { +Widget toolInputBody(SurfaceTokens tokens, String toolName, Map 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 input) { } /// Write tool body: the file path + content with syntax highlighting. -Widget toolWriteBody(SurfaceTokens tokens, Map input) { +Widget toolWriteBody(SurfaceTokens tokens, Map 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 input) { +Widget toolEditBody(SurfaceTokens tokens, Map 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 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 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 input) { +Widget toolReadLikeBody(SurfaceTokens tokens, String toolName, Map input, String mono) { final path = input['file_path'] ?? input['path'] ?? input['pattern'] ?? ''; final extra = []; if (toolName == 'Grep') { @@ -576,13 +576,13 @@ Widget toolReadLikeBody(SurfaceTokens tokens, String toolName, Map 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 ------------------------------------------- diff --git a/lib/builtin/search/src/search_panel_view.dart b/lib/builtin/search/src/search_panel_view.dart index ae25215a..4d843a3a 100644 --- a/lib/builtin/search/src/search_panel_view.dart +++ b/lib/builtin/search/src/search_panel_view.dart @@ -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), ), ), ], diff --git a/lib/builtin/welcome/src/welcome_view.dart b/lib/builtin/welcome/src/welcome_view.dart index ef15571c..dea80761 100644 --- a/lib/builtin/welcome/src/welcome_view.dart +++ b/lib/builtin/welcome/src/welcome_view.dart @@ -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), ], ), ), diff --git a/lib/widgets/src/clide_markdown.dart b/lib/widgets/src/clide_markdown.dart index 990f765e..3d4440ec 100644 --- a/lib/widgets/src/clide_markdown.dart +++ b/lib/widgets/src/clide_markdown.dart @@ -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,