render markdown hard breaks and image placeholders (T-379)
Both node types fell through the inline-span switch to an empty textContent span: words on either side of a hard break glued together, and images vanished with no trace. A br now emits a newline; an img renders a muted italic "[image: alt]" placeholder (falling back to the src) — no inline network loading in the owned renderer; live-pane images keep going through clide image show. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -4007,3 +4007,5 @@ 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 ('06FBHC5ZE4EZEGXK8YY8J86CM0', 'status', 'in_progress', 'done', NULL, '2026-06-11 23:13:18', '2026-06-11 23:13:18', '2026-06-11 23:13:18', NULL, '5382c848b654d1a93daed15a28d652cf', 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 ('06FBHCC6AR37VTF4SY8DR99JHC', 'status', 'backlog', 'in_progress', NULL, '2026-06-11 23:13:49', '2026-06-11 23:13:49', '2026-06-11 23:13:49', NULL, '0edb800f29a8851306f7d21fb546d342', 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 ('06FBHCC6AR37VTF4SY8DR99JHC', 'status', 'in_progress', 'done', NULL, '2026-06-11 23:16:28', '2026-06-11 23:16:28', '2026-06-11 23:16:28', NULL, '2fe291251d1a4789c8d2b75a2c32396f', 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 ('06FBHCJEYHC91PMVNVWVHBR2RG', 'status', 'backlog', 'in_progress', NULL, '2026-06-11 23:16:53', '2026-06-11 23:16:53', '2026-06-11 23:16:53', NULL, 'e6c0cb3a2fce6e257e054f5b3e209814', 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 ('06FBHCJEYHC91PMVNVWVHBR2RG', 'status', 'in_progress', 'done', NULL, '2026-06-11 23:18:22', '2026-06-11 23:18:22', '2026-06-11 23:18:22', NULL, '22809d0fac7e1aa116e682c18dae7455', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
|
||||
@@ -4139,3 +4139,13 @@ INSERT INTO tickets (record_id, type, parent_record_id, title, description, stat
|
||||
Fix: serialize with a real encoder (JSON/YAML emitter, whatever the file format is) covering nested structures; write to a temp file + rename for atomicity; on parse failure keep the original file (e.g. move aside as .broken) and log via the kernel Logger instead of resetting.
|
||||
|
||||
Acceptance: round-trip test for a keymap overlay (list of maps) across save/load; simulated partial write leaves previous settings intact; corrupt file does not silently reset and produces a logged diagnostic.', 'done', 'medium', NULL, NULL, NULL, '2026-06-11 21:58:22', '2026-06-11 23:16:28', NULL, 'e23aad60b9f75e50cb305e727254e094', 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 ('06FBHCJEYHC91PMVNVWVHBR2RG', 'bug', '06FBHBGHNEQTAEPGNJKN42C1E8', 'Markdown renderer: hard breaks and images render as empty text', 'lib/widgets/src/clide_markdown.dart:408-410 — hard line breaks and image nodes both fall through to empty text spans: words on either side of a hard break glue together, and images vanish entirely (no placeholder, no alt text).
|
||||
|
||||
Fix: emit a newline span for hard breaks; render images as at least an alt-text placeholder chip (full image rendering can be a follow-up — note the existing feedback that live-pane images go through clide image show).
|
||||
|
||||
Acceptance: golden/widget test for hard-break line splitting; image node renders alt text; no regression in existing markdown goldens.', 'in_progress', 'medium', NULL, NULL, NULL, '2026-06-11 21:59:13', '2026-06-11 23:16:53', NULL, 'e3bf2108898cf82d75be7a0da17443ca', 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 ('06FBHCJEYHC91PMVNVWVHBR2RG', 'bug', '06FBHBGHNEQTAEPGNJKN42C1E8', 'Markdown renderer: hard breaks and images render as empty text', 'lib/widgets/src/clide_markdown.dart:408-410 — hard line breaks and image nodes both fall through to empty text spans: words on either side of a hard break glue together, and images vanish entirely (no placeholder, no alt text).
|
||||
|
||||
Fix: emit a newline span for hard breaks; render images as at least an alt-text placeholder chip (full image rendering can be a follow-up — note the existing feedback that live-pane images go through clide image show).
|
||||
|
||||
Acceptance: golden/widget test for hard-break line splitting; image node renders alt text; no regression in existing markdown goldens.', 'done', 'medium', NULL, NULL, NULL, '2026-06-11 21:59:13', '2026-06-11 23:18:22', NULL, '5ec0ffc2676c2f31476249301ae2869b', 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);
|
||||
|
||||
@@ -115,6 +115,11 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit.
|
||||
preserved as `.broken` with a logged warning instead of being silently
|
||||
reset. (T-376)
|
||||
|
||||
- **Markdown hard breaks break lines and images leave a visible trace.**
|
||||
Both rendered as empty text — words on either side of a hard break glued
|
||||
together and images vanished; breaks now emit a newline and images render
|
||||
an italic `[image: alt]` placeholder. (T-379)
|
||||
|
||||
- **Accepting ExitPlanMode now leaves plan mode in the conversation panel.**
|
||||
Approving Claude's plan (the ExitPlanMode tool) transitioned the underlying
|
||||
session out of plan mode, but clide's tracked permission mode didn't follow,
|
||||
|
||||
@@ -405,6 +405,22 @@ class ClideMarkdown extends StatelessWidget {
|
||||
text: _unescapeHtml(el.textContent),
|
||||
style: TextStyle(decoration: TextDecoration.lineThrough, color: tokens.globalTextMuted),
|
||||
);
|
||||
case 'br':
|
||||
// A hard break has no textContent — the default branch rendered it
|
||||
// as an empty span and glued the surrounding words together (T-379).
|
||||
return const TextSpan(text: '\n');
|
||||
case 'img':
|
||||
// No inline image loading (network fetch in a text span is not the
|
||||
// owned-renderer way; live-pane images go through `clide image
|
||||
// show`) — render a visible alt-text placeholder instead of
|
||||
// disappearing (T-379).
|
||||
final alt = _unescapeHtml(el.attributes['alt'] ?? '');
|
||||
final src = el.attributes['src'] ?? '';
|
||||
final label = alt.isNotEmpty ? alt : src;
|
||||
return TextSpan(
|
||||
text: label.isEmpty ? '[image]' : '[image: $label]',
|
||||
style: TextStyle(color: tokens.globalTextMuted, fontStyle: FontStyle.italic),
|
||||
);
|
||||
default:
|
||||
return TextSpan(text: _unescapeHtml(el.textContent));
|
||||
}
|
||||
|
||||
@@ -49,4 +49,26 @@ void main() {
|
||||
expect(find.textContaining('https://example.com'), findsOneWidget);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
// T-379: hard breaks and images fell through to empty text spans —
|
||||
// words glued together, images vanished without a trace.
|
||||
testWidgets('a hard line break splits the line instead of gluing words', (tester) async {
|
||||
// Two trailing spaces = a markdown hard break.
|
||||
await tester.pumpWidget(harness(f, const ClideMarkdown('alpha \nbeta')));
|
||||
await tester.pump();
|
||||
expect(find.textContaining('alpha\nbeta'), findsOneWidget);
|
||||
expect(find.textContaining('alphabeta'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('an image renders its alt text as a visible placeholder', (tester) async {
|
||||
await tester.pumpWidget(harness(f, const ClideMarkdown('before  after')));
|
||||
await tester.pump();
|
||||
expect(find.textContaining('[image: a diagram]'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('an image with no alt text falls back to its source', (tester) async {
|
||||
await tester.pumpWidget(harness(f, const ClideMarkdown('')));
|
||||
await tester.pump();
|
||||
expect(find.textContaining('[image: http://x/y.png]'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user