Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
355812e
perf: 优化 CodeDisplay 大内容 Pretty 性能
tesgth032 Mar 2, 2026
7ae0cca
fix: 修复 only-matches 截断与 SSE 展开状态
tesgth032 Mar 2, 2026
6c3fcc7
fix: 加强 Worker 回退与取消健壮性
tesgth032 Mar 2, 2026
783cc98
fix: 避免 JSON pretty 重启与 Worker 挂起
tesgth032 Mar 2, 2026
86cee4b
修复 CodeDisplay Worker 取消与回退中断
tesgth032 Mar 2, 2026
ad154e8
修复 Worker 初始化标记时机
tesgth032 Mar 2, 2026
800dabd
fix: 同步 CodeDisplay 默认模式并优化 SSE 搜索
tesgth032 Mar 2, 2026
6bf05fa
chore: format code (perf-sessions-messages-pretty-800dabd)
github-actions[bot] Mar 2, 2026
bec9e7a
test: 补齐 CodeDisplay Worker 与配置单测
tesgth032 Mar 2, 2026
1d84e4e
chore: format code (perf-sessions-messages-pretty-bec9e7a)
github-actions[bot] Mar 2, 2026
81a8dd6
fix: CodeDisplay 美化重试与下载类型修正
tesgth032 Mar 2, 2026
4148773
fix(code-display): 补齐 CRLF/Worker 错误提示与回退文案
tesgth032 Mar 2, 2026
c7c1fce
fix(code-display): 大内容 Pretty 安全兜底与类型收敛
tesgth032 Mar 2, 2026
c8f80ea
chore: format code (perf-sessions-messages-pretty-c7c1fce)
github-actions[bot] Mar 2, 2026
9dd6f8b
chore: 修正 usage-ledger.test.ts 格式
tesgth032 Mar 2, 2026
9fd2808
chore: format code (perf-sessions-messages-pretty-9dd6f8b)
github-actions[bot] Mar 2, 2026
61ccf5f
fix(code-display): 修复 Pretty Worker 自取消卡住并补测试
tesgth032 Mar 2, 2026
43f0ad5
chore: 固定 Biome 版本并移除 schema 版本绑定
tesgth032 Mar 2, 2026
6dc2672
fix(code-display): only-matches 错误提示与取消轮询一致性
tesgth032 Mar 2, 2026
f13ec01
fix(code-display): 下载 MIME 校验与禁用 Worker 兜底
tesgth032 Mar 2, 2026
b426802
fix(code-display): 索引失败不触发搜索并补取消语义
tesgth032 Mar 2, 2026
3bda57d
perf(code-display): SSE 展开降开销并补强索引/取消
tesgth032 Mar 2, 2026
eb23ccd
refactor(code-display): 提取阈值常量
tesgth032 Mar 2, 2026
693cbee
fix(code-display): 下载 MIME 更保守并优化回退去抖
tesgth032 Mar 2, 2026
db23582
perf(code-display): Worker 队列化并提升取消响应
tesgth032 Mar 2, 2026
1708205
perf(code-display): 虚拟高亮回退原因提示并强化 textKey
tesgth032 Mar 2, 2026
0e30a73
perf(code-display): worker禁用时only-matches走增量索引
tesgth032 Mar 3, 2026
2d70390
chore: 恢复 biome schema 并取消 Biome pin
tesgth032 Mar 3, 2026
8d39c1e
fix(code-display): 修复虚拟高亮透传与 SSE 虚拟化
tesgth032 Mar 3, 2026
030de03
fix(code-display): SSE 虚拟列表自适应行高
tesgth032 Mar 3, 2026
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
17 changes: 17 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,23 @@ STORE_SESSION_RESPONSE_BODY=true # 是否在 Redis 中存储会话响应
# - false:不存储响应体(注意:不影响本次请求处理;仅影响后续查看 response body)
# 说明:该开关不影响内部统计读取响应体(tokens/费用统计、SSE 假 200 检测仍会进行)

# Dashboard CodeDisplay(长内容美化性能)
# 说明:
# - 方案1:长内容 Pretty 视图使用纯文本(textarea)展示,避免语法高亮 DOM 爆炸导致卡顿(默认开启)。
# - 方案3:仅对可视窗口做语法高亮(虚拟化高亮)。默认关闭,需要显式开启;开启后可在 UI 切换“纯文本/虚拟高亮”。
CCH_CODEDISPLAY_LARGE_PLAIN=true
CCH_CODEDISPLAY_VIRTUAL_HIGHLIGHT=false

# 可选调参(一般无需修改)
CCH_CODEDISPLAY_WORKER_ENABLE=true
CCH_CODEDISPLAY_PERF_DEBUG=false
CCH_CODEDISPLAY_HIGHLIGHT_MAX_CHARS=30000
CCH_CODEDISPLAY_VIRTUAL_OVERSCAN_LINES=50
CCH_CODEDISPLAY_VIRTUAL_CONTEXT_LINES=50
CCH_CODEDISPLAY_VIRTUAL_LINE_HEIGHT_PX=18
CCH_CODEDISPLAY_MAX_PRETTY_OUTPUT_BYTES=20000000
CCH_CODEDISPLAY_MAX_LINE_INDEX_LINES=200000

# 熔断器配置
# 功能说明:控制网络错误是否计入熔断器失败计数
# - false (默认):网络错误(DNS 解析失败、连接超时、代理连接失败等)不计入熔断器,仅供应商错误(4xx/5xx HTTP 响应)计入
Expand Down
27 changes: 27 additions & 0 deletions messages/en/dashboard.json
Original file line number Diff line number Diff line change
Expand Up @@ -568,7 +568,27 @@
"codeDisplay": {
"raw": "Raw",
"pretty": "Pretty",
"prettyWorking": "Formatting... {percent}%",
"viewPlain": "Plain",
"viewVirtual": "Virtual highlight",
"virtualFallbackToPlain": "Virtual highlight unavailable, switched to plain",
"searchPlaceholder": "Search",
"searchWorking": "Searching... {percent}%",
"search": {
"indexFailed": "Failed to build search index",
"indexCanceled": "Search index build canceled",
"indexTooManyLines": "Too many lines ({lineCount}, limit {maxLines})",
"indexTooManyLinesUnknown": "Too many lines (limit {maxLines})",
"failed": "Search failed",
"canceled": "Search canceled"
},
"cancel": "Cancel",
"retry": "Retry",
"prettyCanceled": "Formatting canceled",
"prettyFailed": "Formatting failed",
"prettyInvalidJson": "Invalid JSON",
"prettyOutputTooLarge": "Formatted output too large",
"prettyWorkerUnavailable": "Worker unavailable, cannot format very large JSON",
"expand": "Expand",
"collapse": "Collapse",
"themeAuto": "Auto",
Expand All @@ -582,6 +602,13 @@
"pageInfo": "Page {page} / {total}",
"sseEvent": "Event",
"sseData": "Data",
"virtual": {
"indexWorking": "Indexing... {percent}%",
"indexFailed": "Failed to build index, switched to plain",
"indexCanceled": "Indexing canceled, switched to plain",
"indexTooManyLines": "Too many lines ({lineCount}, max {maxLines}), switched to plain",
"indexTooManyLinesUnknown": "Too many lines (max {maxLines}), switched to plain"
},
"hardLimit": {
"title": "Content too large",
"size": "Size: {sizeMB} MB ({sizeBytes} bytes)",
Expand Down
27 changes: 27 additions & 0 deletions messages/ja/dashboard.json
Original file line number Diff line number Diff line change
Expand Up @@ -568,7 +568,27 @@
"codeDisplay": {
"raw": "Raw",
"pretty": "Pretty",
"prettyWorking": "整形中... {percent}%",
"viewPlain": "テキスト",
"viewVirtual": "仮想ハイライト",
"virtualFallbackToPlain": "仮想ハイライトを利用できないため、テキスト表示に切り替えました",
"searchPlaceholder": "検索",
"searchWorking": "検索中... {percent}%",
"search": {
"indexFailed": "検索インデックスの作成に失敗しました",
"indexCanceled": "検索インデックスの作成をキャンセルしました",
"indexTooManyLines": "行数が多すぎます({lineCount} 行、上限 {maxLines} 行)",
"indexTooManyLinesUnknown": "行数が多すぎます(上限 {maxLines} 行)",
"failed": "検索に失敗しました",
"canceled": "検索をキャンセルしました"
},
"cancel": "キャンセル",
"retry": "再試行",
"prettyCanceled": "整形をキャンセルしました",
"prettyFailed": "整形に失敗しました",
"prettyInvalidJson": "無効な JSON",
"prettyOutputTooLarge": "整形結果が大きすぎます",
"prettyWorkerUnavailable": "Worker を利用できないため、大きな JSON を整形できません",
"expand": "展開",
"collapse": "折りたたむ",
"themeAuto": "自動",
Expand All @@ -582,6 +602,13 @@
"pageInfo": "{page} / {total} ページ",
"sseEvent": "イベント",
"sseData": "データ",
"virtual": {
"indexWorking": "索引作成中... {percent}%",
"indexFailed": "索引の作成に失敗したため、テキスト表示に切り替えました",
"indexCanceled": "索引作成をキャンセルしたため、テキスト表示に切り替えました",
"indexTooManyLines": "行数が多すぎます ({lineCount} 行、上限 {maxLines} 行)。テキスト表示に切り替えました",
"indexTooManyLinesUnknown": "行数が多すぎます (上限 {maxLines} 行)。テキスト表示に切り替えました"
},
"hardLimit": {
"title": "コンテンツが大きすぎます",
"size": "サイズ: {sizeMB} MB ({sizeBytes} bytes)",
Expand Down
27 changes: 27 additions & 0 deletions messages/ru/dashboard.json
Original file line number Diff line number Diff line change
Expand Up @@ -568,7 +568,27 @@
"codeDisplay": {
"raw": "Сырой",
"pretty": "Форматированный",
"prettyWorking": "Форматирование... {percent}%",
"viewPlain": "Текст",
"viewVirtual": "Виртуальная подсветка",
"virtualFallbackToPlain": "Виртуальная подсветка недоступна — переключено на простой текст",
"searchPlaceholder": "Поиск",
"searchWorking": "Поиск... {percent}%",
"search": {
"indexFailed": "Не удалось построить индекс поиска",
"indexCanceled": "Построение индекса поиска отменено",
"indexTooManyLines": "Слишком много строк ({lineCount}, лимит {maxLines})",
"indexTooManyLinesUnknown": "Слишком много строк (лимит {maxLines})",
"failed": "Поиск не удался",
"canceled": "Поиск отменён"
},
"cancel": "Отмена",
"retry": "Повторить",
"prettyCanceled": "Форматирование отменено",
"prettyFailed": "Ошибка форматирования",
"prettyInvalidJson": "Некорректный JSON",
"prettyOutputTooLarge": "Результат форматирования слишком большой",
"prettyWorkerUnavailable": "Worker недоступен — невозможно отформатировать большой JSON",
"expand": "Развернуть",
"collapse": "Свернуть",
"themeAuto": "Авто",
Expand All @@ -582,6 +602,13 @@
"pageInfo": "Страница {page} / {total}",
"sseEvent": "Событие",
"sseData": "Данные",
"virtual": {
"indexWorking": "Индексация... {percent}%",
"indexFailed": "Не удалось построить индекс — переключено на простой текст",
"indexCanceled": "Индексация отменена — переключено на простой текст",
"indexTooManyLines": "Слишком много строк ({lineCount}, максимум {maxLines}) — переключено на простой текст",
"indexTooManyLinesUnknown": "Слишком много строк (максимум {maxLines}) — переключено на простой текст"
},
"hardLimit": {
"title": "Содержимое слишком большое",
"size": "Размер: {sizeMB} MB ({sizeBytes} bytes)",
Expand Down
27 changes: 27 additions & 0 deletions messages/zh-CN/dashboard.json
Original file line number Diff line number Diff line change
Expand Up @@ -568,7 +568,27 @@
"codeDisplay": {
"raw": "原始",
"pretty": "美化",
"prettyWorking": "美化中... {percent}%",
"viewPlain": "纯文本",
"viewVirtual": "虚拟高亮",
"virtualFallbackToPlain": "虚拟高亮不可用,已回退到纯文本",
"searchPlaceholder": "搜索",
"searchWorking": "搜索中... {percent}%",
"search": {
"indexFailed": "建立搜索索引失败",
"indexCanceled": "已取消建立搜索索引",
"indexTooManyLines": "内容行数过多({lineCount} 行,上限 {maxLines} 行)",
"indexTooManyLinesUnknown": "内容行数过多(上限 {maxLines} 行)",
"failed": "搜索失败",
"canceled": "已取消搜索"
},
"cancel": "取消",
"retry": "重试",
"prettyCanceled": "已取消美化",
"prettyFailed": "美化失败",
"prettyInvalidJson": "无效的 JSON",
"prettyOutputTooLarge": "美化结果过大",
"prettyWorkerUnavailable": "当前环境无法使用 Worker,无法美化超大 JSON",
"expand": "展开",
"collapse": "收起",
"themeAuto": "跟随系统",
Expand All @@ -582,6 +602,13 @@
"pageInfo": "第 {page} / {total} 页",
"sseEvent": "事件",
"sseData": "数据",
"virtual": {
"indexWorking": "建立索引中... {percent}%",
"indexFailed": "建立索引失败,已回退到纯文本",
"indexCanceled": "已取消建立索引,已回退到纯文本",
"indexTooManyLines": "内容行数过多({lineCount} 行,上限 {maxLines} 行),已回退到纯文本",
"indexTooManyLinesUnknown": "内容行数过多(上限 {maxLines} 行),已回退到纯文本"
},
"hardLimit": {
"title": "内容过大",
"size": "大小:{sizeMB} MB({sizeBytes} 字节)",
Expand Down
27 changes: 27 additions & 0 deletions messages/zh-TW/dashboard.json
Original file line number Diff line number Diff line change
Expand Up @@ -568,7 +568,27 @@
"codeDisplay": {
"raw": "原始內容",
"pretty": "格式化",
"prettyWorking": "格式化中... {percent}%",
"viewPlain": "純文字",
"viewVirtual": "虛擬高亮",
"virtualFallbackToPlain": "虛擬高亮不可用,已回退到純文字",
"searchPlaceholder": "搜尋",
"searchWorking": "搜尋中... {percent}%",
"search": {
"indexFailed": "建立搜尋索引失敗",
"indexCanceled": "已取消建立搜尋索引",
"indexTooManyLines": "內容行數過多({lineCount} 行,上限 {maxLines} 行)",
"indexTooManyLinesUnknown": "內容行數過多(上限 {maxLines} 行)",
"failed": "搜尋失敗",
"canceled": "已取消搜尋"
},
"cancel": "取消",
"retry": "重試",
"prettyCanceled": "已取消美化",
"prettyFailed": "美化失敗",
"prettyInvalidJson": "無效的 JSON",
"prettyOutputTooLarge": "美化結果過大",
"prettyWorkerUnavailable": "目前環境無法使用 Worker,無法美化超大 JSON",
"expand": "展開",
"collapse": "摺疊",
"themeAuto": "跟隨系統",
Expand All @@ -582,6 +602,13 @@
"pageInfo": "第 {page} / {total} 頁",
"sseEvent": "事件類型",
"sseData": "資料",
"virtual": {
"indexWorking": "建立索引中... {percent}%",
"indexFailed": "建立索引失敗,已回退到純文字",
"indexCanceled": "已取消建立索引,已回退到純文字",
"indexTooManyLines": "內容行數過多({lineCount} 行,上限 {maxLines} 行),已回退到純文字",
"indexTooManyLinesUnknown": "內容行數過多(上限 {maxLines} 行),已回退到純文字"
},
"hardLimit": {
"title": "內容過大",
"size": "大小:{sizeMB} MB({sizeBytes} 位元組)",
Expand Down
4 changes: 3 additions & 1 deletion src/app/[locale]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { notFound } from "next/navigation";
import { NextIntlClientProvider } from "next-intl";
import { getMessages } from "next-intl/server";
import { Footer } from "@/components/customs/footer";
import { parseCodeDisplayConfigFromEnv } from "@/components/ui/code-display-config";
import { Toaster } from "@/components/ui/sonner";
import { type Locale, locales } from "@/i18n/config";
import { logger } from "@/lib/logger";
Expand Down Expand Up @@ -72,14 +73,15 @@ export default async function RootLayout({
// Load translation messages
const messages = await getMessages();
const timeZone = await resolveSystemTimezone();
const codeDisplayConfig = parseCodeDisplayConfigFromEnv(process.env);
// Create a stable `now` timestamp to avoid SSR/CSR hydration mismatch for relative time
const now = new Date();

return (
<html lang={locale} suppressHydrationWarning>
<body className="antialiased">
<NextIntlClientProvider messages={messages} timeZone={timeZone} now={now}>
<AppProviders>
<AppProviders codeDisplayConfig={codeDisplayConfig}>
<div className="flex min-h-[var(--cch-viewport-height,100vh)] flex-col bg-background text-foreground">
<div className="flex-1">{children}</div>
<Footer />
Expand Down
29 changes: 17 additions & 12 deletions src/app/providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { Agentation } from "agentation";
import { ThemeProvider } from "next-themes";
import { type ReactNode, useState } from "react";
import type { CodeDisplayConfig } from "@/components/ui/code-display-config";
import { CodeDisplayConfigProvider } from "@/components/ui/code-display-config-context";

interface AppProvidersProps {
children: ReactNode;
codeDisplayConfig?: CodeDisplayConfig;
}

export const QUERY_CLIENT_DEFAULTS = {
Expand All @@ -18,7 +21,7 @@ export const QUERY_CLIENT_DEFAULTS = {
},
};

export function AppProviders({ children }: AppProvidersProps) {
export function AppProviders({ children, codeDisplayConfig }: AppProvidersProps) {
const [queryClient] = useState(
() =>
new QueryClient({
Expand All @@ -28,17 +31,19 @@ export function AppProviders({ children }: AppProvidersProps) {

return (
<QueryClientProvider client={queryClient}>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
storageKey="claude-code-hub-theme"
enableColorScheme
disableTransitionOnChange
>
{children}
{process.env.NODE_ENV === "development" && <Agentation />}
</ThemeProvider>
<CodeDisplayConfigProvider value={codeDisplayConfig}>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
storageKey="claude-code-hub-theme"
enableColorScheme
disableTransitionOnChange
>
{children}
{process.env.NODE_ENV === "development" && <Agentation />}
</ThemeProvider>
</CodeDisplayConfigProvider>
</QueryClientProvider>
);
}
40 changes: 33 additions & 7 deletions src/components/ui/__tests__/code-display.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,21 @@ function renderWithIntl(node: ReactNode) {
document.body.appendChild(container);
const root = createRoot(container);

act(() => {
root.render(
<NextIntlClientProvider locale="en" messages={messages} timeZone="UTC">
{node}
</NextIntlClientProvider>
);
});
const render = (next: ReactNode) => {
act(() => {
root.render(
<NextIntlClientProvider locale="en" messages={messages} timeZone="UTC">
{next}
</NextIntlClientProvider>
);
});
};

render(node);

return {
container,
rerender: render,
unmount: () => {
act(() => root.unmount());
container.remove();
Expand Down Expand Up @@ -64,6 +69,27 @@ describe("CodeDisplay", () => {
unmount();
});

test("updates default mode when language changes", async () => {
const raw = '{"a":1,"b":2}';
const { container, rerender, unmount } = renderWithIntl(
<CodeDisplay content={raw} language="text" fileName="response.txt" />
);

// text 默认 raw
expect(container.textContent).toContain(raw);
expect(container.textContent).not.toContain('"a": 1');

rerender(<CodeDisplay content={raw} language="json" fileName="response.json" />);

// 等待 useEffect 同步默认模式
await act(async () => {
await new Promise((r) => setTimeout(r, 0));
});

expect(container.textContent).toContain('"a": 1');
unmount();
});

test("raw mode renders HTML-like content as text (no script/img elements)", () => {
const malicious = `<script>alert("XSS")</script><img src=x onerror=alert('XSS')>Hello`;
const { container, unmount } = renderWithIntl(
Expand Down
27 changes: 27 additions & 0 deletions src/components/ui/code-display-config-context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"use client";

import { createContext, useContext } from "react";
import {
type CodeDisplayConfig,
DEFAULT_CODE_DISPLAY_CONFIG,
} from "@/components/ui/code-display-config";

const CodeDisplayConfigContext = createContext<CodeDisplayConfig>(DEFAULT_CODE_DISPLAY_CONFIG);

export function CodeDisplayConfigProvider({
value,
children,
}: {
value?: CodeDisplayConfig;
children: React.ReactNode;
}) {
return (
<CodeDisplayConfigContext.Provider value={value ?? DEFAULT_CODE_DISPLAY_CONFIG}>
{children}
</CodeDisplayConfigContext.Provider>
);
}

export function useCodeDisplayConfig(): CodeDisplayConfig {
return useContext(CodeDisplayConfigContext);
}
Loading
Loading