From b7efaabcdc5ff0e8b4a37733475fd5043e3b1a5c Mon Sep 17 00:00:00 2001 From: rechard-guo Date: Tue, 7 Jul 2026 11:29:38 +0800 Subject: [PATCH 1/6] Feature(Desktop): #974 Add ctrl+F keyboard shortcut to open find-in-page --- desktop/src/components/layout/Sidebar.tsx | 2 + .../src/components/search/FindInPageModal.tsx | 203 ++++++++++++++++++ desktop/src/hooks/useKeyboardShortcuts.ts | 6 + 3 files changed, 211 insertions(+) create mode 100644 desktop/src/components/search/FindInPageModal.tsx diff --git a/desktop/src/components/layout/Sidebar.tsx b/desktop/src/components/layout/Sidebar.tsx index 9a692dd5..7a8cfce1 100644 --- a/desktop/src/components/layout/Sidebar.tsx +++ b/desktop/src/components/layout/Sidebar.tsx @@ -5,6 +5,7 @@ import { useUIStore } from '../../stores/uiStore' import { useTranslation, type TranslationKey } from '../../i18n' import { ConfirmDialog } from '../shared/ConfirmDialog' import { GlobalSearchModal } from '../search/GlobalSearchModal' +import { FindInPageModal } from '../search/FindInPageModal' import type { SessionListItem } from '../../types/session' import { useTabStore, SETTINGS_TAB_ID, SCHEDULED_TAB_ID } from '../../stores/tabStore' import { useChatStore } from '../../stores/chatStore' @@ -1208,6 +1209,7 @@ export function Sidebar({ isMobile = false, onRequestClose }: SidebarProps) { /> + ) } diff --git a/desktop/src/components/search/FindInPageModal.tsx b/desktop/src/components/search/FindInPageModal.tsx new file mode 100644 index 00000000..f38b1718 --- /dev/null +++ b/desktop/src/components/search/FindInPageModal.tsx @@ -0,0 +1,203 @@ +import { useEffect, useRef, useState } from 'react' +import { createPortal } from 'react-dom' +import { ChevronDown, ChevronUp, Search, X } from 'lucide-react' + +// JS-based scoped find-in-page. We walk text nodes in the document body EXCLUDING the +// sidebar (.sidebar-panel) and this find bar ([data-find-bar]), then highlight matches +// with the CSS Custom Highlight API (Range-based, no DOM mutation → React-safe). +// Why not native webContents.findInPage: it scans the whole document incl. the +// value, so the search box matches itself and steals focus/caret. Scoping sidesteps that. + +const FIND_DEBOUNCE_MS = 250 +const RESULTS_HL = 'cc-find-results' +const ACTIVE_HL = 'cc-find-active' +// Subtrees never searched: sidebar, tab bar, this find bar, non-content tags. +const SKIP_CLOSEST = '.sidebar-panel, [data-testid="tab-bar"], [data-find-bar], script, style, noscript, .material-symbols-outlined' + +type Props = { + open: boolean + onClose: () => void +} + +export function FindInPageModal({ open, onClose }: Props) { + const [query, setQuery] = useState('') + const [debouncedQuery, setDebouncedQuery] = useState('') + const [count, setCount] = useState(0) + const [activeIndex, setActiveIndex] = useState(0) + const inputRef = useRef(null) + const rangesRef = useRef([]) + + // Focus + reset whenever the bar opens; clear highlights when it closes. + useEffect(() => { + if (!open) { + clearHighlights() + return + } + setQuery('') + setDebouncedQuery('') + setCount(0) + setActiveIndex(0) + rangesRef.current = [] + const id = requestAnimationFrame(() => inputRef.current?.focus()) + return () => cancelAnimationFrame(id) + }, [open]) + + // Debounce the typed query. + useEffect(() => { + const id = setTimeout(() => setDebouncedQuery(query), FIND_DEBOUNCE_MS) + return () => clearTimeout(id) + }, [query]) + + // Clear highlights on unmount. + useEffect(() => () => clearHighlights(), []) + + // Run (or clear) the search once the query settles. + useEffect(() => { + const q = debouncedQuery.trim() + if (!q) { + clearHighlights() + rangesRef.current = [] + setCount(0) + setActiveIndex(0) + return + } + const ranges = collectRanges(q) + rangesRef.current = ranges + setCount(ranges.length) + setActiveIndex(0) + paint(ranges, 0) + }, [debouncedQuery]) + + // Next/previous — immediate, uses live state. + function step(forward: boolean) { + const ranges = rangesRef.current + if (ranges.length === 0) return + const next = forward ? (activeIndex + 1) % ranges.length : (activeIndex - 1 + ranges.length) % ranges.length + setActiveIndex(next) + paint(ranges, next) + } + + function onKeyDown(e: React.KeyboardEvent) { + if (e.key === 'Enter') { + e.preventDefault() + step(!e.shiftKey) // Enter = next, Shift+Enter = previous + } else if (e.key === 'Escape') { + e.preventDefault() + onClose() + } + } + + if (!open) return null + + return createPortal( +
+ +
+
+
, + document.body, + ) +} + +// ---- search core (module scope, no React state) ---- + +/** Walk visible text nodes outside skipped subtrees; return a Range per case-insensitive match. */ +function collectRanges(q: string): Range[] { + const ranges: Range[] = [] + const needle = q.toLowerCase() + const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, { + acceptNode(node) { + const parent = node.parentElement + if (!parent || !node.nodeValue || !node.nodeValue.trim()) return NodeFilter.FILTER_REJECT + if (parent.closest(SKIP_CLOSEST)) return NodeFilter.FILTER_REJECT + return NodeFilter.FILTER_ACCEPT + }, + }) + let textNode = walker.nextNode() as Text | null + while (textNode) { + const text = textNode.nodeValue!.toLowerCase() + let idx = text.indexOf(needle) + while (idx !== -1) { + const range = document.createRange() + range.setStart(textNode, idx) + range.setEnd(textNode, idx + needle.length) + ranges.push(range) + idx = text.indexOf(needle, idx + needle.length) + } + textNode = walker.nextNode() as Text | null + } + return ranges +} + +/** Register CSS highlights for all matches + the active one, and scroll the active into view. */ +function paint(ranges: Range[], activeIndex: number) { + const highlights = (CSS as any).highlights as Map | undefined + const HighlightCtor = (globalThis as any).Highlight + if (highlights && HighlightCtor) { + const results = new HighlightCtor() + for (const r of ranges) results.add(r) + highlights.set(RESULTS_HL, results) + const active = ranges[activeIndex] + if (active) { + const activeHl = new HighlightCtor() + activeHl.add(active) + activeHl.priority = 1 // paint over the results highlight + highlights.set(ACTIVE_HL, activeHl) + } else { + highlights.delete(ACTIVE_HL) + } + } + ranges[activeIndex]?.startContainer.parentElement?.scrollIntoView({ block: 'center', behavior: 'smooth' }) +} + +function clearHighlights() { + const highlights = (CSS as any).highlights as Map | undefined + highlights?.delete(RESULTS_HL) + highlights?.delete(ACTIVE_HL) +} diff --git a/desktop/src/hooks/useKeyboardShortcuts.ts b/desktop/src/hooks/useKeyboardShortcuts.ts index 8a299e78..5382c3bc 100644 --- a/desktop/src/hooks/useKeyboardShortcuts.ts +++ b/desktop/src/hooks/useKeyboardShortcuts.ts @@ -56,6 +56,12 @@ export function useKeyboardShortcuts() { openModal('globalSearch') } + // Ctrl+F — Open find-in-page bar + if (meta && e.key === 'f') { + e.preventDefault() + openModal('findInPage') + } + // Escape — Close modal or clear state if (e.key === 'Escape') { if (activeModalRef.current) { From 97b3c4041d323fb91e809215f70e88e839e95055 Mon Sep 17 00:00:00 2001 From: zhb <50901800+zhbdesign@users.noreply.github.com> Date: Thu, 9 Jul 2026 14:55:27 +0800 Subject: [PATCH 2/6] =?UTF-8?q?=E6=AC=A2=E8=BF=8E=E8=AF=8D=E4=BC=98?= =?UTF-8?q?=E5=8C=96=EF=BC=8C=E6=94=B9=E4=B8=BA"=E6=9E=84=E5=BB=BA?= =?UTF-8?q?=E3=80=81=E8=B0=83=E8=AF=95=E5=92=8C=E8=AE=BE=E8=AE=A1"?= =?UTF-8?q?=E6=9B=B4=E7=AC=A6=E5=90=88=E4=B8=AD=E6=96=87=E8=A1=A8=E8=BE=BE?= =?UTF-8?q?=E4=B9=A0=E6=83=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 欢迎词优化,改为"构建、调试和设计"更符合中文表达习惯 --- desktop/src/i18n/locales/zh.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desktop/src/i18n/locales/zh.ts b/desktop/src/i18n/locales/zh.ts index 1b5b096a..2469199a 100644 --- a/desktop/src/i18n/locales/zh.ts +++ b/desktop/src/i18n/locales/zh.ts @@ -1144,7 +1144,7 @@ export const zh: Record = { // ─── Empty Session ────────────────────────────────────── 'empty.title': '新建会话', - 'empty.subtitle': '开始一个新的编码会话。Claude 已准备好帮你构建、调试和架构你的项目。', + 'empty.subtitle': '开始一个新的编码会话。Claude 已准备好帮你构建、调试和设计你的项目。', 'empty.placeholder': '随便问点什么...', 'empty.addFiles': '添加文件或图片', 'empty.slashCommands': '斜杠命令', From 2ab20e997492754c0069abdc27881f7bb5f33705 Mon Sep 17 00:00:00 2001 From: zhb <50901800+zhbdesign@users.noreply.github.com> Date: Thu, 9 Jul 2026 14:56:36 +0800 Subject: [PATCH 3/6] =?UTF-8?q?=E6=AC=A2=E8=BF=8E=E8=AF=8D=E4=BC=98?= =?UTF-8?q?=E5=8C=96=EF=BC=8C=E6=94=B9=E4=B8=BA"=E6=9E=84=E5=BB=BA?= =?UTF-8?q?=E3=80=81=E8=B0=83=E8=AF=95=E5=92=8C=E8=AE=BE=E8=AE=A1"?= =?UTF-8?q?=E6=9B=B4=E7=AC=A6=E5=90=88=E4=B8=AD=E6=96=87=E8=A1=A8=E8=BE=BE?= =?UTF-8?q?=E4=B9=A0=E6=83=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 欢迎词优化,改为"构建、调试和设计"更符合中文表达习惯 --- desktop/src/i18n/locales/zh-TW.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desktop/src/i18n/locales/zh-TW.ts b/desktop/src/i18n/locales/zh-TW.ts index 2503af48..e414c0d5 100644 --- a/desktop/src/i18n/locales/zh-TW.ts +++ b/desktop/src/i18n/locales/zh-TW.ts @@ -1144,7 +1144,7 @@ export const zh: Record = { // ─── Empty Session ────────────────────────────────────── 'empty.title': '新建會話', - 'empty.subtitle': '開始一個新的編碼會話。Claude 已準備好幫你構建、除錯和架構你的專案。', + 'empty.subtitle': '開始一個新的編碼會話。Claude 已準備好幫你構建、除錯和設計你的專案。', 'empty.placeholder': '隨便問點什麼...', 'empty.addFiles': '新增檔案或圖片', 'empty.slashCommands': '斜槓命令', From 902b2e3d135af850b9dfdf882729c72597e2a393 Mon Sep 17 00:00:00 2001 From: zhb <50901800+zhbdesign@users.noreply.github.com> Date: Thu, 9 Jul 2026 15:26:42 +0800 Subject: [PATCH 4/6] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dtoken=E7=94=A8=E9=87=8F?= =?UTF-8?q?=E6=9C=80=E9=95=BF=E4=BB=BB=E5=8A=A1=E6=97=B6=E9=95=BF=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=E7=9C=81=E7=95=A5=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复token用量最长任务时长显示省略号 --- desktop/src/pages/ActivitySettings.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/desktop/src/pages/ActivitySettings.tsx b/desktop/src/pages/ActivitySettings.tsx index bd473316..334e6925 100644 --- a/desktop/src/pages/ActivitySettings.tsx +++ b/desktop/src/pages/ActivitySettings.tsx @@ -772,9 +772,9 @@ export function ActivitySettings() { style={{ animationDelay: `${index * 45}ms` }} >
-
+ }`} style={{ overflow: 'visible', whiteSpace: 'normal', textOverflow: 'clip' }}> {metric.value}
From 83f8c6623312632bf04811d015dbd7a902fbbef7 Mon Sep 17 00:00:00 2001 From: zhb <50901800+zhbdesign@users.noreply.github.com> Date: Thu, 9 Jul 2026 16:23:07 +0800 Subject: [PATCH 5/6] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dmermaid=E6=B8=B2=E6=9F=93?= =?UTF-8?q?=E9=81=97=E6=BC=8F=E4=BA=86=20/=EF=BC=88=E6=96=9C=E6=9D=A0?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复mermaid渲染遗漏了 /(斜杠) --- desktop/src/components/chat/MermaidRenderer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desktop/src/components/chat/MermaidRenderer.tsx b/desktop/src/components/chat/MermaidRenderer.tsx index 8c5151dd..4489b05c 100644 --- a/desktop/src/components/chat/MermaidRenderer.tsx +++ b/desktop/src/components/chat/MermaidRenderer.tsx @@ -40,7 +40,7 @@ type MermaidThemeColors = { const FLOWCHART_START = /^\s*(?:graph|flowchart)\b/i const FLOWCHART_NODE_START = /^([A-Za-z][\w-]*)\[/ -const UNQUOTED_FLOWCHART_LABEL_UNSAFE = /|[{}[\]*]/i +const UNQUOTED_FLOWCHART_LABEL_UNSAFE = /|[{}[\]*\/]/i function isFlowchartDiagram(code: string) { const firstMeaningfulLine = code From 3e7c7de54a03627465dc3c093b5d91e051d98bfd Mon Sep 17 00:00:00 2001 From: zhb <50901800+zhbdesign@users.noreply.github.com> Date: Thu, 9 Jul 2026 17:54:24 +0800 Subject: [PATCH 6/6] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dmermaid=E6=B8=B2=E6=9F=93?= =?UTF-8?q?=E9=81=97=E6=BC=8F=E4=BA=86=20/=EF=BC=88=E6=96=9C=E6=9D=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复mermaid渲染遗漏了 /(斜杠 --- .../chat/MermaidRenderer.integration.test.tsx | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/desktop/src/components/chat/MermaidRenderer.integration.test.tsx b/desktop/src/components/chat/MermaidRenderer.integration.test.tsx index eafb81c8..137491e6 100644 --- a/desktop/src/components/chat/MermaidRenderer.integration.test.tsx +++ b/desktop/src/components/chat/MermaidRenderer.integration.test.tsx @@ -58,6 +58,24 @@ describe('MermaidRenderer Mermaid integration', () => { expect(surface.innerHTML).not.toContain('onerror') }) + it('auto-quotes flowchart labels containing forward slashes to avoid lexical errors', async () => { + render( + D2[直接调用 dclService]', + ' D2 --> R[返回结果]', + ].join('\n')} + />, + ) + + const surface = await screen.findByTestId('mermaid-diagram-surface') + + expect(surface).toHaveTextContent('/api/dcl') + expect(surface).toHaveTextContent('直接调用 dclService') + expect(screen.queryByText('Mermaid Error')).not.toBeInTheDocument() + }) + it('renders generated flowchart labels with HTML breaks and structural characters', async () => { render(