From 9783e6bd2c03389cff3784a888ab6a0c20eb0a36 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Sat, 23 May 2026 00:34:19 +0200 Subject: [PATCH] render user messages as a distinct card in the conversation (T-143) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User turns rendered flat (a "you" label + text on the canvas), the same shape as Claude's responses, so prompts were hard to pick out when scanning. UserMessage now renders in a card: a left accent stripe (focus colour) and a filled background distinct from the panel canvas. Claude's text responses stay flat markdown — better for reading long answers, and the asymmetry makes "what I asked" easy to spot. Co-Authored-By: Claude Opus 4.7 (1M context) --- .pql/changelog/ticket_history/2026-05.sql | 2 + .pql/changelog/tickets/2026-05.sql | 2 + CHANGELOG.md | 3 ++ lib/builtin/claude/src/conversation_view.dart | 42 ++++++++++++++++++- 4 files changed, 48 insertions(+), 1 deletion(-) diff --git a/.pql/changelog/ticket_history/2026-05.sql b/.pql/changelog/ticket_history/2026-05.sql index f61514b0..12a25e63 100644 --- a/.pql/changelog/ticket_history/2026-05.sql +++ b/.pql/changelog/ticket_history/2026-05.sql @@ -2144,3 +2144,5 @@ INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-138', 'status', 'backlog', 'in_progress', NULL, '2026-05-22 21:08:05', '2026-05-22 21:08:05', '2026-05-22 21:08:05', NULL, 'c3dfefe24d912081cd89ac0be066b641', 1) ON CONFLICT(hash) DO NOTHING; INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-138', 'status', 'in_progress', 'done', NULL, '2026-05-22 21:49:09', '2026-05-22 21:49:09', '2026-05-22 21:49:09', NULL, 'e4be02e339d638da70a4e7ce0f850691', 1) ON CONFLICT(hash) DO NOTHING; INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-142', 'status', 'backlog', 'in_progress', NULL, '2026-05-22 22:09:09', '2026-05-22 22:09:09', '2026-05-22 22:09:09', NULL, 'b799657bf436bc44e306ef71658c80e5', 1) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-142', 'status', 'in_progress', 'done', NULL, '2026-05-22 22:18:02', '2026-05-22 22:18:02', '2026-05-22 22:18:02', NULL, '374be08610920e404c8394bc10b1e12a', 1) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-143', 'status', 'backlog', 'in_progress', NULL, '2026-05-22 22:31:25', '2026-05-22 22:31:25', '2026-05-22 22:31:25', NULL, 'e2a16eb8af0afc30a5d0fc88c523e507', 1) ON CONFLICT(hash) DO NOTHING; diff --git a/.pql/changelog/tickets/2026-05.sql b/.pql/changelog/tickets/2026-05.sql index 53c71701..ad08139d 100644 --- a/.pql/changelog/tickets/2026-05.sql +++ b/.pql/changelog/tickets/2026-05.sql @@ -2489,3 +2489,5 @@ INSERT INTO tickets (id, type, parent_id, title, description, status, priority, INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-138', 'task', 'T-132', 'Native composer + file/image paste', 'New lib/builtin/claude/src/claude_composer.dart: native text field -> tmux -L clide send-keys -t (Enter submit, Shift+Enter newline). Paste text inline; file/image per the T2 spike finding (likely temp-file + path/@-reference, since send-keys carries text only). Acceptance: typing in the composer reaches Claude; pasting a text file and an image both work via the validated mechanism. Blocked by T2, T5.', 'in_progress', 'medium', NULL, NULL, NULL, '2026-05-22 15:59:34', '2026-05-22 21:08:05', NULL, '2e6b932ad78ac78d83c86c5e08290ed3', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_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 (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-138', 'task', 'T-132', 'Native composer + file/image paste', 'New lib/builtin/claude/src/claude_composer.dart: native text field -> tmux -L clide send-keys -t (Enter submit, Shift+Enter newline). Paste text inline; file/image per the T2 spike finding (likely temp-file + path/@-reference, since send-keys carries text only). Acceptance: typing in the composer reaches Claude; pasting a text file and an image both work via the validated mechanism. Blocked by T2, T5.', 'done', 'medium', NULL, NULL, NULL, '2026-05-22 15:59:34', '2026-05-22 21:49:09', NULL, '8b247ce6f9fc63c6f56030303aece2e8', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_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 (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-142', 'task', 'T-132', 'Composer attachment chips (thumbnail/icon on paste)', 'Show pasted files/images as chips above the composer input instead of inserting the @path as text. Image chips render a thumbnail (Image.file of the cache/temp file); other types show a Phosphor file icon + basename; each chip has a remove ×. On submit, the typed text plus the chips'' @path tokens are assembled into the message sent via tmux.sendMessage, then text + chips clear. Typed @path text still works. resolveClipboardAttachment changes to return a list of attachment descriptors (path + isImage). No new package deps (Image.file + existing Phosphor icons). Builds on T-138.', 'in_progress', 'medium', NULL, NULL, NULL, '2026-05-22 22:06:59', '2026-05-22 22:09:09', NULL, 'da746aa3a523e3b351eaa13f19fa51e1', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_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 (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-142', 'task', 'T-132', 'Composer attachment chips (thumbnail/icon on paste)', 'Show pasted files/images as chips above the composer input instead of inserting the @path as text. Image chips render a thumbnail (Image.file of the cache/temp file); other types show a Phosphor file icon + basename; each chip has a remove ×. On submit, the typed text plus the chips'' @path tokens are assembled into the message sent via tmux.sendMessage, then text + chips clear. Typed @path text still works. resolveClipboardAttachment changes to return a list of attachment descriptors (path + isImage). No new package deps (Image.file + existing Phosphor icons). Builds on T-138.', 'done', 'medium', NULL, NULL, NULL, '2026-05-22 22:06:59', '2026-05-22 22:18:02', NULL, '384c57378557c6f32f2cf93af99873c4', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_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 (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-143', 'task', 'T-132', 'Distinguish user message cards in the conversation', 'User messages render flat (label + text on the canvas) like Claude''s responses, so it''s hard to scan what you said vs what Claude said. Give UserMessage a distinct card: a filled background (globalBackground vs the panelBackground canvas), a left accent stripe (globalFocus), padding and rounded corners. Claude''s text responses stay flat markdown (better for long answers). conversation_view.dart _ConversationTurn.', 'in_progress', 'low', NULL, NULL, NULL, '2026-05-22 22:31:22', '2026-05-22 22:31:25', NULL, 'efa90d878f79235f81d54d00c04014c8', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_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 0a8d7ada..4ba345b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -86,6 +86,9 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit. ### Changed +- User messages in the Claude conversation now render as a distinct card + (accent stripe + filled background) so your prompts read apart from + Claude's flat-markdown responses (T-143). - Claude conversation content now flows through the kernel MessageBus — a reader tails the transcript and publishes items; the pane subscribes. Decouples reading from rendering so the upcoming team panels can show diff --git a/lib/builtin/claude/src/conversation_view.dart b/lib/builtin/claude/src/conversation_view.dart index 2f88b23d..98f02765 100644 --- a/lib/builtin/claude/src/conversation_view.dart +++ b/lib/builtin/claude/src/conversation_view.dart @@ -105,7 +105,7 @@ class _ConversationTurn extends StatelessWidget { Widget build(BuildContext context) { final i = item; return switch (i) { - UserMessage() => _labelled('you', tokens.globalForeground, ClideMarkdown(i.text)), + UserMessage() => _userCard(i), AssistantTextMessage() => _labelled('claude', tokens.globalFocus, ClideMarkdown(i.text)), AssistantThinkingMessage() => _labelled( 'thinking', @@ -117,6 +117,46 @@ class _ConversationTurn extends StatelessWidget { }; } + /// The user's own message — a distinct card (accent stripe + fill) so + /// it reads apart from Claude's flat-markdown responses. + Widget _userCard(UserMessage m) { + return Padding( + padding: const EdgeInsets.only(bottom: 14), + child: ClipRRect( + borderRadius: BorderRadius.circular(6), + child: ColoredBox( + color: tokens.globalBackground, + child: IntrinsicHeight( + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Container(width: 3, color: tokens.globalFocus), + Expanded( + child: Padding( + padding: const EdgeInsets.fromLTRB(12, 8, 12, 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ClideText( + 'you', + fontSize: clideFontSmall, + color: tokens.globalFocus, + fontFamily: clideMonoFamily, + ), + const SizedBox(height: 4), + ClideMarkdown(m.text), + ], + ), + ), + ), + ], + ), + ), + ), + ), + ); + } + /// A labelled turn: a small role tag above the body. Widget _labelled(String label, Color labelColor, Widget body) { return Padding(