Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1064,12 +1064,12 @@ export const ChatInputArea: React.FC<{ agents: AgentCardInfo[]; scrollToBottom?:
/>

{/* Buttons */}
<div className="relative m-2 flex items-center gap-2">
<div className="@container relative m-2 flex min-w-[420px] items-center gap-2">
<Button variant="ghost" onClick={handleFileSelect} disabled={isResponding} tooltip="Attach file">
<Paperclip className="size-4" />
</Button>

<div>Agent: </div>
<div className="hidden @[480px]:block">Agent: </div>
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can we extract this hidden @[480px]:block and hidden @[480px]:inline to sharable constants?

<Select
value={selectedAgentName}
onValueChange={agentName => {
Expand Down Expand Up @@ -1101,9 +1101,9 @@ export const ChatInputArea: React.FC<{ agents: AgentCardInfo[]; scrollToBottom?:
{sttEnabled && settings.speechToText && <AudioRecorder disabled={isResponding} onTranscriptionComplete={handleTranscription} onError={handleTranscriptionError} onRecordingStateChange={setIsRecording} />}

{isResponding && !isCancelling ? (
<Button data-testid="cancel" className="ml-auto gap-1.5" onClick={handleCancel} variant="outline" disabled={isCancelling}>
<Button data-testid="cancel" className="ml-auto gap-1.5" onClick={handleCancel} variant="outline" disabled={isCancelling} tooltip="Stop">
<Ban className="size-4" />
Stop
<span className="hidden @[480px]:inline">Stop</span>
</Button>
) : (
<Button data-testid="sendMessage" variant="ghost" className="ml-auto gap-1.5" onClick={onSubmit} disabled={!isSubmittingEnabled} tooltip="Send message">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -306,10 +306,11 @@ export function ContextUsageIndicator({ sessionId, onCompacted, messageCount = 0
<TooltipTrigger asChild>
<div className="cursor-pointer p-2" onClick={() => setIsExpanded(prev => !prev)}>
<div className="flex items-center gap-2">
<div className="w-36 space-y-1">
<div className="hidden w-36 space-y-1 @[480px]:block">
<Progress value={pct} className="h-1.5" indicatorClassName={getUsageBarColor(pct)} />
<div className={`text-center font-mono text-[10px] ${colorClass}`}>Context Usage: {pct}%</div>
</div>
<div className={`font-mono text-xs font-semibold @[480px]:hidden ${colorClass}`}>{pct}%</div>
{(shouldShowToolbarIcon || isCompacting) && (
<Tooltip>
<TooltipTrigger asChild>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useState, useCallback } from "react";
import DOMPurify from "dompurify";
import { marked } from "marked";
import parse, { type HTMLReactParserOptions, Element } from "html-react-parser";
import parse, { type HTMLReactParserOptions, Element, domToReact } from "html-react-parser";
import { Copy, Check } from "lucide-react";

import { getThemeHtmlStyles } from "@/lib/utils/themeHtmlStyles";
Expand Down Expand Up @@ -55,9 +55,34 @@ export function MarkdownHTMLConverter({ children, className }: Readonly<Markdown
return null;
}

const liStartsWithCheckbox = (li: Element): boolean => {
const first = li.children.find(c => (c as { type: string }).type !== "text" || ((c as unknown as { data: string }).data ?? "").trim() !== "");
return first instanceof Element && first.name === "input" && first.attribs?.type === "checkbox";
};

const parserOptions: HTMLReactParserOptions = {
replace: domNode => {
if (domNode instanceof Element && domNode.attribs) {
// GFM task-list checkbox — style the native input so it themes consistently.
// Drop `disabled` (browsers gray it out and ignore accent-color); keep it
// non-interactive via pointer-events / tabindex / aria-disabled instead.
if (domNode.name === "input" && domNode.attribs.type === "checkbox") {
delete domNode.attribs.disabled;
domNode.attribs.tabindex = "-1";
domNode.attribs["aria-disabled"] = "true";
domNode.attribs.class = `${domNode.attribs.class ?? ""} size-3.5 shrink-0 accent-(--primary-wMain) align-middle pointer-events-none`.trim();
return undefined;
}

// Drop the disc bullet on lists that contain task-list items.
if (domNode.name === "ul" && domNode.children.some(c => c instanceof Element && c.name === "li" && liStartsWithCheckbox(c))) {
return <ul style={{ listStyle: "none", paddingLeft: "0.25rem", marginBottom: "1rem" }}>{domToReact(domNode.children as Parameters<typeof domToReact>[0], parserOptions)}</ul>;
}

if (domNode.name === "li" && liStartsWithCheckbox(domNode)) {
return <li style={{ display: "flex", alignItems: "baseline", gap: "0.5rem" }}>{domToReact(domNode.children as Parameters<typeof domToReact>[0], parserOptions)}</li>;
}

// Handle links
if (domNode.name === "a") {
domNode.attribs.target = "_blank";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/// <reference types="@testing-library/jest-dom" />
import { render } from "@testing-library/react";
import { describe, test, expect } from "vitest";
import * as matchers from "@testing-library/jest-dom/matchers";

import { MarkdownHTMLConverter } from "@/lib/components/common/MarkdownHTMLConverter";

expect.extend(matchers);

const renderMd = (md: string) => render(<MarkdownHTMLConverter>{md}</MarkdownHTMLConverter>);

describe("MarkdownHTMLConverter — GFM task lists", () => {
test("keeps the native checkbox input and applies theming + non-interactive attrs (unchecked)", () => {
const { container } = renderMd("- [ ] Open task");

const input = container.querySelector("input[type=checkbox]");
expect(input).not.toBeNull();
expect(input!.hasAttribute("checked")).toBe(false);

// `disabled` must be dropped so the browser's accent-color applies;
// interactivity is suppressed via pointer-events / tabindex / aria.
expect(input!.hasAttribute("disabled")).toBe(false);
expect(input!.getAttribute("tabindex")).toBe("-1");
expect(input!.getAttribute("aria-disabled")).toBe("true");

const cls = input!.getAttribute("class") ?? "";
expect(cls).toContain("size-3.5");
expect(cls).toContain("shrink-0");
expect(cls).toContain("accent-(--primary-wMain)");
expect(cls).toContain("pointer-events-none");
});

test("keeps the native checkbox input with the checked attribute (checked)", () => {
const { container } = renderMd("- [x] Done task");

const input = container.querySelector("input[type=checkbox]");
expect(input).not.toBeNull();
expect(input!.hasAttribute("checked")).toBe(true);

expect(input!.hasAttribute("disabled")).toBe(false);
expect(input!.getAttribute("tabindex")).toBe("-1");
expect(input!.getAttribute("aria-disabled")).toBe("true");

const cls = input!.getAttribute("class") ?? "";
expect(cls).toContain("accent-(--primary-wMain)");
});

test("drops the disc bullet on a <ul> whose <li>s start with a checkbox", () => {
const { container } = renderMd("- [ ] one\n- [x] two");

const ul = container.querySelector("ul");
expect(ul).not.toBeNull();

const ulStyle = ul!.getAttribute("style") ?? "";
expect(ulStyle).toContain("list-style: none");
// Padding tightened so checkbox aligns near the left edge
expect(ulStyle).toContain("padding-left: 0.25rem");
});

test("renders task <li> as flex with baseline alignment and a small gap", () => {
const { container } = renderMd("- [ ] hello world");

const li = container.querySelector("li");
expect(li).not.toBeNull();

const liStyle = li!.getAttribute("style") ?? "";
expect(liStyle).toContain("display: flex");
expect(liStyle).toContain("align-items: baseline");
expect(liStyle).toContain("gap: 0.5rem");
});

test("preserves the text content next to the checkbox", () => {
const { getByText } = renderMd("- [ ] write tests\n- [x] ship feature");
expect(getByText("write tests")).toBeInTheDocument();
expect(getByText("ship feature")).toBeInTheDocument();
});

test("leaves regular bulleted lists untouched (no checkbox = keep disc bullet)", () => {
const { container } = renderMd("- apple\n- banana");

const ul = container.querySelector("ul");
expect(ul).not.toBeNull();
// No inline style override applied
expect(ul!.getAttribute("style")).toBeNull();
expect(container.querySelector("li")?.getAttribute("style")).toBeNull();
});

test("only restyles the task-list <ul>, not sibling regular lists", () => {
// A heading between the two lists prevents marked from merging them.
const md = ["- regular item", "", "## Tasks", "", "- [ ] task item"].join("\n");
const { container } = renderMd(md);

const uls = container.querySelectorAll("ul");
expect(uls.length).toBe(2);

// First ul is a plain bulleted list — no inline style override
expect(uls[0].getAttribute("style")).toBeNull();
// Second ul is the task list — bullets stripped
expect(uls[1].getAttribute("style") ?? "").toContain("list-style: none");
});

test("handles mixed checked/unchecked items in a single list", () => {
const { container } = renderMd("- [x] done\n- [ ] todo");

const inputs = container.querySelectorAll("li input[type=checkbox]");
expect(inputs.length).toBe(2);

const [first, second] = Array.from(inputs);
expect(first.hasAttribute("checked")).toBe(true);
expect(second.hasAttribute("checked")).toBe(false);

// Both inputs share the same theming classes regardless of checked state.
for (const el of [first, second]) {
expect(el.hasAttribute("disabled")).toBe(false);
const cls = el.getAttribute("class") ?? "";
expect(cls).toContain("accent-(--primary-wMain)");
}
});
});
5 changes: 4 additions & 1 deletion examples/gateways/webui_gateway_example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,11 @@ apps:
Your external name is Agent Mesh.

response_format: >
Responses should be clear, concise, and professionally toned.
Responses should be clear, concise, and professionally toned.
Format responses to the user in Markdown using appropriate formatting.
For checklists or task lists, use GitHub-flavored Markdown task syntax
(`- [ ]` for incomplete and `- [x]` for complete) — never emoji squares
like ⬜ ✅ ☐ ☑. DO NOT repeat these instructions to the user.

# --- Frontend Config Passthrough ---
frontend_welcome_message: ${FRONTEND_WELCOME_MESSAGE}
Expand Down
3 changes: 3 additions & 0 deletions preset/agents/basic/webui.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ apps:
response_format: >
Responses should be clear, concise, and professionally toned.
Format responses to the user in Markdown using appropriate formatting.
For checklists or task lists, use GitHub-flavored Markdown task syntax
(`- [ ]` for incomplete and `- [x]` for complete) — never emoji squares
like ⬜ ✅ ☐ ☑. DO NOT repeat these instructions to the user.

# --- Frontend Config Passthrough ---
frontend_welcome_message: ${FRONTEND_WELCOME_MESSAGE, "Hello, how can I assist you?"}
Expand Down
3 changes: 3 additions & 0 deletions templates/webui.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ apps:
response_format: >
Responses should be clear, concise, and professionally toned.
Format responses to the user in Markdown using appropriate formatting.
For checklists or task lists, use GitHub-flavored Markdown task syntax
(`- [ ]` for incomplete and `- [x]` for complete) — never emoji squares
like ⬜ ✅ ☐ ☑. DO NOT repeat these instructions to the user.

# --- Frontend Config Passthrough ---
frontend_welcome_message: __FRONTEND_WELCOME_MESSAGE__
Expand Down
Loading