fix: strip thinking tags in chat UI

This commit is contained in:
Peter Steinberger
2026-01-09 05:24:11 +00:00
parent 7842109609
commit 133124e023
5 changed files with 89 additions and 8 deletions

View File

@@ -30,6 +30,7 @@ describe("chat markdown rendering", () => {
const app = mountApp("/chat");
await app.updateComplete;
const timestamp = Date.now();
app.chatMessages = [
{
role: "assistant",
@@ -37,9 +38,11 @@ describe("chat markdown rendering", () => {
{ type: "toolcall", name: "noop", arguments: {} },
{ type: "toolresult", name: "noop", text: "Hello **world**" },
],
timestamp: Date.now(),
timestamp,
},
];
// Expand the tool output card so its markdown is rendered into the DOM.
app.toolOutputExpanded = new Set([`${timestamp}:1`]);
await app.updateComplete;
@@ -47,4 +50,3 @@ describe("chat markdown rendering", () => {
expect(strong?.textContent).toBe("world");
});
});

View File

@@ -1,4 +1,5 @@
import type { GatewayBrowserClient } from "../gateway";
import { stripThinkingTags } from "../format";
import { generateUUID } from "../uuid";
export type ChatState = {
@@ -127,8 +128,11 @@ export function handleChatEvent(
function extractText(message: unknown): string | null {
const m = message as Record<string, unknown>;
const role = typeof m.role === "string" ? m.role : "";
const content = m.content;
if (typeof content === "string") return content;
if (typeof content === "string") {
return role === "assistant" ? stripThinkingTags(content) : content;
}
if (Array.isArray(content)) {
const parts = content
.map((p) => {
@@ -137,8 +141,13 @@ function extractText(message: unknown): string | null {
return null;
})
.filter((v): v is string => typeof v === "string");
if (parts.length > 0) return parts.join("\n");
if (parts.length > 0) {
const joined = parts.join("\n");
return role === "assistant" ? stripThinkingTags(joined) : joined;
}
}
if (typeof m.text === "string") {
return role === "assistant" ? stripThinkingTags(m.text) : m.text;
}
if (typeof m.text === "string") return m.text;
return null;
}

25
ui/src/ui/format.test.ts Normal file
View File

@@ -0,0 +1,25 @@
import { describe, expect, it } from "vitest";
import { stripThinkingTags } from "./format";
describe("stripThinkingTags", () => {
it("strips <think>…</think> segments", () => {
const input = ["<think>", "secret", "</think>", "", "Hello"].join("\n");
expect(stripThinkingTags(input)).toBe("Hello");
});
it("strips <thinking>…</thinking> segments", () => {
const input = ["<thinking>", "secret", "</thinking>", "", "Hello"].join("\n");
expect(stripThinkingTags(input)).toBe("Hello");
});
it("keeps text when tags are unpaired", () => {
expect(stripThinkingTags("<think>\nsecret\nHello")).toBe("secret\nHello");
expect(stripThinkingTags("Hello\n</think>")).toBe("Hello\n");
});
it("returns original text when no tags exist", () => {
expect(stripThinkingTags("Hello")).toBe("Hello");
});
});

View File

@@ -66,3 +66,39 @@ export function parseList(input: string): string[] {
.map((v) => v.trim())
.filter((v) => v.length > 0);
}
const THINKING_TAG_RE = /<\s*\/?\s*think(?:ing)?\s*>/gi;
const THINKING_OPEN_RE = /<\s*think(?:ing)?\s*>/i;
const THINKING_CLOSE_RE = /<\s*\/\s*think(?:ing)?\s*>/i;
export function stripThinkingTags(value: string): string {
if (!value) return value;
const hasOpen = THINKING_OPEN_RE.test(value);
const hasClose = THINKING_CLOSE_RE.test(value);
if (!hasOpen && !hasClose) return value;
// If we don't have a balanced pair, avoid dropping trailing content.
if (hasOpen !== hasClose) {
if (!hasOpen) return value.replace(THINKING_CLOSE_RE, "").trimStart();
return value.replace(THINKING_OPEN_RE, "").trimStart();
}
if (!THINKING_TAG_RE.test(value)) return value;
THINKING_TAG_RE.lastIndex = 0;
let result = "";
let lastIndex = 0;
let inThinking = false;
for (const match of value.matchAll(THINKING_TAG_RE)) {
const idx = match.index ?? 0;
if (!inThinking) {
result += value.slice(lastIndex, idx);
}
const tag = match[0].toLowerCase();
inThinking = !tag.includes("/");
lastIndex = idx + match[0].length;
}
if (!inThinking) {
result += value.slice(lastIndex);
}
return result.trimStart();
}

View File

@@ -2,6 +2,7 @@ import { html, nothing } from "lit";
import { repeat } from "lit/directives/repeat.js";
import { unsafeHTML } from "lit/directives/unsafe-html.js";
import { stripThinkingTags } from "../format";
import { toSanitizedMarkdownHtml } from "../markdown";
import { formatToolDetail, resolveToolDisplay } from "../tool-display";
import type { SessionsListResult } from "../types";
@@ -388,8 +389,11 @@ function renderMessage(
function extractText(message: unknown): string | null {
const m = message as Record<string, unknown>;
const role = typeof m.role === "string" ? m.role : "";
const content = m.content;
if (typeof content === "string") return content;
if (typeof content === "string") {
return role === "assistant" ? stripThinkingTags(content) : content;
}
if (Array.isArray(content)) {
const parts = content
.map((p) => {
@@ -398,9 +402,14 @@ function extractText(message: unknown): string | null {
return null;
})
.filter((v): v is string => typeof v === "string");
if (parts.length > 0) return parts.join("\n");
if (parts.length > 0) {
const joined = parts.join("\n");
return role === "assistant" ? stripThinkingTags(joined) : joined;
}
}
if (typeof m.text === "string") {
return role === "assistant" ? stripThinkingTags(m.text) : m.text;
}
if (typeof m.text === "string") return m.text;
return null;
}