Skip to content

feat: render task notifications as styled cards#122

Open
Psypeal wants to merge 1 commit intomatt1398:mainfrom
Psypeal:feat/task-notification-rendering
Open

feat: render task notifications as styled cards#122
Psypeal wants to merge 1 commit intomatt1398:mainfrom
Psypeal:feat/task-notification-rendering

Conversation

@Psypeal
Copy link
Contributor

@Psypeal Psypeal commented Mar 18, 2026

Summary

Parse <task-notification> XML blocks from user messages and render them as compact styled cards instead of raw XML.

Closes #121

Before

Raw XML displayed verbatim in user message bubbles — confusing and noisy.

After

Compact cards showing: status icon (green check / red X), command name, exit code, and output filename.

Changes

  • contentSanitizer.ts: Add parseTaskNotifications() parser + strip <task-notification> tags and trailing "Read the output file..." instructions from display text
  • UserChatGroup.tsx: Parse task notifications from original message content, render as styled cards below the user message bubble

Test plan

  • pnpm typecheck — no type errors
  • pnpm lint:fix — no lint errors
  • All 653 tests pass
  • Manual: open a session with background tasks — notifications render as cards instead of raw XML
  • Manual: session with no task notifications — no change in rendering

Summary by CodeRabbit

  • New Features
    • Task notifications now parse and display in chat with visual status indicators (success, pending, failure)
    • Shows command names, exit codes, and optional output file information for each task
    • Status-based color coding and icons provide quick visual feedback on task completion

Parse <task-notification> XML blocks from user messages and render
them as compact cards with status icon, command name, exit code,
and output filename — instead of displaying raw XML.

- Add parseTaskNotifications() to contentSanitizer.ts
- Strip <task-notification> tags and trailing output instructions
  from display text
- Render parsed notifications in UserChatGroup.tsx as styled cards

Closes matt1398#121

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@gemini-code-assist
Copy link

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the user experience by transforming raw, verbose XML task notifications into clear, concise, and visually appealing cards within the chat interface. This change improves readability and reduces visual clutter, making it easier for users to quickly grasp the status and details of background tasks without being exposed to technical XML markup.

Highlights

  • Task Notification Rendering: Implemented the parsing of <task-notification> XML blocks from user messages and their rendering as compact, styled cards instead of raw XML.
  • Content Sanitization: Updated contentSanitizer.ts to include a new parseTaskNotifications() function, strip <task-notification> tags, and remove trailing 'Read the output file...' instructions from display text.
  • User Interface Integration: Modified UserChatGroup.tsx to parse task notifications from original message content and render them as visually distinct cards below the user message bubble.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a great feature to render task notifications as styled cards, which significantly improves user experience by replacing raw XML with a structured and readable format. The implementation is well-structured, with parsing logic cleanly separated in contentSanitizer.ts and rendering handled in the UserChatGroup.tsx component.

I've identified a few areas for improvement, mainly related to performance and code clarity. Specifically, I've suggested moving regular expression creations out of loops/render functions to avoid unnecessary re-compilation. I've also pointed out a small refinement in a type guard for better code conciseness.

Overall, this is a solid contribution. Addressing these minor points will make the code more efficient and maintainable.

? userGroup.message.content
: Array.isArray(userGroup.message.content)
? userGroup.message.content
.filter((b): b is { type: 'text'; text: string } => b.type === 'text' && 'text' in b)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The type guard here is a bit verbose. If userGroup.message.content is an array of a discriminated union based on the type property, the check b.type === 'text' should be sufficient for TypeScript to infer that b has a text property. The additional 'text' in b check is likely redundant and can be removed for cleaner code.

Suggested change
.filter((b): b is { type: 'text'; text: string } => b.type === 'text' && 'text' in b)
.filter((b): b is { type: 'text'; text: string } => b.type === 'text')

Comment on lines +493 to +497
const cmdMatch = /"([^"]+)"/.exec(notif.summary);
const cmdName = cmdMatch?.[1] ?? notif.summary;
// Extract exit code
const exitMatch = /\(exit code (\d+)\)/.exec(notif.summary);
const exitCode = exitMatch?.[1];

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For performance, these regular expressions should be defined as constants outside of the component render function. This prevents them from being re-created on every render and for every item in the taskNotifications array.

For example, you can define them at the top of the file:

const CMD_NAME_REGEX = /"([^"]+)"/;
const EXIT_CODE_REGEX = /\(exit code (\d+)\)/;

Then use them inside the map callback:

const cmdMatch = CMD_NAME_REGEX.exec(notif.summary);
// ...
const exitMatch = EXIT_CODE_REGEX.exec(notif.summary);

Comment on lines +182 to +198
export function parseTaskNotifications(content: string): TaskNotification[] {
const notifications: TaskNotification[] = [];
const pattern = /<task-notification>([\s\S]*?)<\/task-notification>/gi;
let match;

while ((match = pattern.exec(content)) !== null) {
const block = match[1];
notifications.push({
taskId: /<task-id>([^<]*)<\/task-id>/.exec(block)?.[1] ?? '',
status: /<status>([^<]*)<\/status>/.exec(block)?.[1] ?? '',
summary: /<summary>([\s\S]*?)<\/summary>/.exec(block)?.[1]?.trim() ?? '',
outputFile: /<output-file>([^<]*)<\/output-file>/.exec(block)?.[1] ?? '',
});
}

return notifications;
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For improved performance and code clarity, the regular expressions used for parsing should be defined once before the while loop. Re-creating them on each iteration is inefficient, especially if a message contains multiple task notifications.

export function parseTaskNotifications(content: string): TaskNotification[] {
  const notifications: TaskNotification[] = [];
  const pattern = /<task-notification>([\s\S]*?)<\/task-notification>/gi;
  const taskIdRegex = /<task-id>([^<]*)<\/task-id>/;
  const statusRegex = /<status>([^<]*)<\/status>/;
  const summaryRegex = /<summary>([\s\S]*?)<\/summary>/;
  const outputFileRegex = /<output-file>([^<]*)<\/output-file>/;
  let match;

  while ((match = pattern.exec(content)) !== null) {
    const block = match[1];
    notifications.push({
      taskId: taskIdRegex.exec(block)?.[1] ?? '',
      status: statusRegex.exec(block)?.[1] ?? '',
      summary: summaryRegex.exec(block)?.[1]?.trim() ?? '',
      outputFile: outputFileRegex.exec(block)?.[1] ?? '',
    });
  }

  return notifications;
}

@coderabbitai coderabbitai bot added the feature request New feature or request label Mar 18, 2026
@coderabbitai
Copy link

coderabbitai bot commented Mar 18, 2026

📝 Walkthrough

Walkthrough

The PR adds support for parsing and rendering task notification XML blocks as styled UI cards. It introduces a TaskNotification interface and parseTaskNotifications utility function to extract notification data, and updates the chat renderer to display notifications with status icons, command names, exit codes, and output file information.

Changes

Cohort / File(s) Summary
Task Notification Utilities
src/shared/utils/contentSanitizer.ts
Introduces TaskNotification interface with taskId, status, summary, and outputFile fields; adds parseTaskNotifications() function to extract task-notification XML blocks from content; extends sanitization logic to strip task-notification tags and trailing "Read the output file..." instructions via new TASK_OUTPUT_INSTRUCTION_PATTERN regex.
Task Notification Rendering
src/renderer/components/chat/UserChatGroup.tsx
Parses task notifications from raw message content and renders them as styled cards alongside chat content, with status-derived icons (CheckCircle, Circle, XCircle), extracted command names from summaries, optional exit codes, and output file paths displayed with status-based color coding.

Suggested labels

feature request

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The pull request successfully implements all three coding objectives from issue #121: parsing task-notification XML blocks, stripping raw XML and trailing instructions from display text, and rendering styled notification cards.
Out of Scope Changes check ✅ Passed All changes are directly aligned with the linked issue objectives. No unrelated modifications to other features, configuration, or unrelated components were introduced.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Tip

CodeRabbit can generate a title for your PR based on the changes with custom instructions.

Set the reviews.auto_title_instructions setting to generate a title for your PR based on the changes in the PR with custom instructions.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (4)
src/renderer/components/chat/UserChatGroup.tsx (2)

521-521: Loose equality check for exitCode display.

exitCode != null uses loose equality which catches both null and undefined. Since exitCode is derived from a regex match (exitMatch?.[1]), it will be undefined when no match is found. The loose check works but consider using strict equality for clarity:

{exitCode !== undefined && <span>exit {exitCode}</span>}

This is a minor stylistic preference.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/chat/UserChatGroup.tsx` at line 521, The conditional
rendering uses a loose equality check for exitCode; update the check in the
UserChatGroup component to use strict comparison against undefined (since
exitCode comes from exitMatch?.[1]) — replace the `exitCode != null` condition
with `exitCode !== undefined` so the display logic is explicit about handling
undefined values from `exitMatch?.[1]`.

499-531: Consider keyboard accessibility for task notification cards.

The cards are currently non-interactive divs. If users might want to interact with them (e.g., click to view output file), consider making them focusable or actionable. For now, as purely informational displays, this is acceptable.

The truncation of outputFile with .split('/').pop() (line 525) works for Unix paths but would fail to extract the filename correctly on Windows paths with backslashes.

♻️ Cross-platform filename extraction
-                        <span className="truncate">{notif.outputFile.split('/').pop()}</span>
+                        <span className="truncate">{notif.outputFile.split(/[/\\]/).pop()}</span>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/chat/UserChatGroup.tsx` around lines 499 - 531, The
task card uses notif.outputFile.split('/').pop() which fails on Windows paths
and the card is a non-interactive div; fix by extracting the filename with a
cross-platform helper (e.g., add a getFileName(path) util used where
notif.outputFile is rendered that splits on both '/' and '\' or uses a regex
like /[\\/]/ to return the last segment), and if the card should be actionable
make the outer div (the container rendering StatusIcon/FileText/cmdName)
keyboard-accessible by converting it to a button/link or adding tabIndex and a
role plus key handlers for Enter/Space (refer to the component rendering the
card in UserChatGroup.tsx and the notif.outputFile/FileText usage).
src/shared/utils/contentSanitizer.ts (2)

182-198: Regex uses global flag with exec() - ensure stateless usage.

The pattern regex has the g flag, and exec() is called in a while loop. This works correctly here because pattern is declared inside the function, so each call gets a fresh regex. Good implementation.

However, the inner field extraction regexes (lines 190-193) don't use [\s\S]*? consistently - <summary> uses it but <task-id>, <status>, and <output-file> use [^<]*. This means multi-line content in those fields would fail to match.

If task-id, status, or output-file could contain newlines or XML-like content, consider using [\s\S]*? for consistency:

♻️ Suggested consistency improvement
     notifications.push({
-      taskId: /<task-id>([^<]*)<\/task-id>/.exec(block)?.[1] ?? '',
-      status: /<status>([^<]*)<\/status>/.exec(block)?.[1] ?? '',
+      taskId: /<task-id>([\s\S]*?)<\/task-id>/.exec(block)?.[1]?.trim() ?? '',
+      status: /<status>([\s\S]*?)<\/status>/.exec(block)?.[1]?.trim() ?? '',
       summary: /<summary>([\s\S]*?)<\/summary>/.exec(block)?.[1]?.trim() ?? '',
-      outputFile: /<output-file>([^<]*)<\/output-file>/.exec(block)?.[1] ?? '',
+      outputFile: /<output-file>([\s\S]*?)<\/output-file>/.exec(block)?.[1]?.trim() ?? '',
     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/shared/utils/contentSanitizer.ts` around lines 182 - 198, In
parseTaskNotifications, the inner field-extraction regexes for taskId, status,
and outputFile use [^<]* which fails on multi-line content; update the three
regexes (/\<task-id\>.../, /\<status\>.../, /\<output-file\>.../) to use
non-greedy multi-line capture ([\s\S]*?) (and apply .trim() like summary where
appropriate) so they correctly capture content with newlines or XML-like text
while keeping the outer <task-notification> loop and fallbacks unchanged.

25-30: Pattern may not match all path formats.

The pattern [^\s]+ matches any non-whitespace, but file paths could potentially contain spaces (especially on Windows or in quoted paths). Additionally, the leading optional space ? might leave behind residual whitespace when the instruction appears mid-content.

Consider whether edge cases with quoted or escaped paths need handling, though for most Unix-style paths this should work correctly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/shared/utils/contentSanitizer.ts` around lines 25 - 30,
TASK_OUTPUT_INSTRUCTION_PATTERN is too narrow: it uses ` ?` and `[^\s]+` which
won't match paths containing spaces or quoted paths and may leave residual
whitespace; update the regex used in TASK_OUTPUT_INSTRUCTION_PATTERN to trim any
surrounding whitespace and accept quoted or unquoted paths that may contain
spaces (for example: match optional leading whitespace, the literal "Read the
output file to retrieve the result:", optional space, then either a quoted
string "(?:\"[^\"]+\"|'[^']+') or an unquoted path that captures any non-newline
characters until whitespace or line end), keep the global flag, and ensure the
pattern removes the whole instruction including surrounding whitespace so no
extra spaces remain after replacement.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/renderer/components/chat/UserChatGroup.tsx`:
- Line 521: The conditional rendering uses a loose equality check for exitCode;
update the check in the UserChatGroup component to use strict comparison against
undefined (since exitCode comes from exitMatch?.[1]) — replace the `exitCode !=
null` condition with `exitCode !== undefined` so the display logic is explicit
about handling undefined values from `exitMatch?.[1]`.
- Around line 499-531: The task card uses notif.outputFile.split('/').pop()
which fails on Windows paths and the card is a non-interactive div; fix by
extracting the filename with a cross-platform helper (e.g., add a
getFileName(path) util used where notif.outputFile is rendered that splits on
both '/' and '\' or uses a regex like /[\\/]/ to return the last segment), and
if the card should be actionable make the outer div (the container rendering
StatusIcon/FileText/cmdName) keyboard-accessible by converting it to a
button/link or adding tabIndex and a role plus key handlers for Enter/Space
(refer to the component rendering the card in UserChatGroup.tsx and the
notif.outputFile/FileText usage).

In `@src/shared/utils/contentSanitizer.ts`:
- Around line 182-198: In parseTaskNotifications, the inner field-extraction
regexes for taskId, status, and outputFile use [^<]* which fails on multi-line
content; update the three regexes (/\<task-id\>.../, /\<status\>.../,
/\<output-file\>.../) to use non-greedy multi-line capture ([\s\S]*?) (and apply
.trim() like summary where appropriate) so they correctly capture content with
newlines or XML-like text while keeping the outer <task-notification> loop and
fallbacks unchanged.
- Around line 25-30: TASK_OUTPUT_INSTRUCTION_PATTERN is too narrow: it uses ` ?`
and `[^\s]+` which won't match paths containing spaces or quoted paths and may
leave residual whitespace; update the regex used in
TASK_OUTPUT_INSTRUCTION_PATTERN to trim any surrounding whitespace and accept
quoted or unquoted paths that may contain spaces (for example: match optional
leading whitespace, the literal "Read the output file to retrieve the result:",
optional space, then either a quoted string "(?:\"[^\"]+\"|'[^']+') or an
unquoted path that captures any non-newline characters until whitespace or line
end), keep the global flag, and ensure the pattern removes the whole instruction
including surrounding whitespace so no extra spaces remain after replacement.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0e0c69a8-0ef5-49e6-9baf-cebc1d214412

📥 Commits

Reviewing files that changed from the base of the PR and between 6a655b9 and a789734.

📒 Files selected for processing (2)
  • src/renderer/components/chat/UserChatGroup.tsx
  • src/shared/utils/contentSanitizer.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature request New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: render task notifications as styled cards instead of raw XML

1 participant