claude-devtools-rs / ui
CLAUDE.md1 starsChanged 6 months agoMITView on GitHub
仅在 Claude 读写 ui/** 下的文件时由 Claude Code 自动加载(子目录 CLAUDE.md on-demand 机制)。跨域共识在根 CLAUDE.md。
- Reads credentials
- Deletes or force-pushes
In this repository31 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- crates/CLAUDE.mdCLAUDE.md
- openspec/CLAUDE.mdCLAUDE.md
- src-tauri/CLAUDE.mdCLAUDE.md
- bug-huntSkill
- bump-versionSkill
- debug-firstSkill
- dep-updateSkill
- e2e-http-verifySkill
- insta-reviewSkill
- mark-tasks-doneSkill
- openspec-apply-changeSkill
- openspec-archive-changeSkill
- openspec-bulk-archive-changeSkill
- openspec-continue-changeSkill
- openspec-exploreSkill
- openspec-ff-changeSkill
- openspec-new-changeSkill
- openspec-onboardSkill
- openspec-proposeSkill
- openspec-sync-specsSkill
- openspec-verify-changeSkill
- perfSkill
- port-dashboardSkill
- preflightSkill
- refactorSkill
- release-runbookSkill
- ts-parity-checkSkill
- ts-ui-compareSkill
- wait-ciSkill
The file
Files
ui/ — Svelte 5 + Vite 前端
仅在 Claude 读写 ui/** 下的文件时由 Claude Code 自动加载(子目录 CLAUDE.md on-demand 机制)。跨域共识在根 CLAUDE.md。
架构与布局
- chrome 三层:
UnifiedTitleBar(顶部 44 px,仅 macOS Tauri 桌面/mock 内部 80 px 让位 traffic light——HTTP server mode?http=1浏览器下无 traffic light 不让位,判定 SHALL 同时含isTauriRuntime()而非只看 UA;含 ProjectSwitcher / UpdateStatusPill / RosettaStatusIcon / 通知 / 设置)+ Sidebar(200~500 px 可拖拽)+ TabBar(pane 内独立,仅 tab 列表,不再含通知/设置/traffic-light padding)+ Main。 - 页面:SessionDetail、SettingsView、NotificationsView、DashboardView(项目卡片网格,替代空状态)。Tab 类型 4 种:session / settings / notifications / memory(后三者单例)。
- chrome 组件细节(change
unified-title-bar):UnifiedTitleBar.svelte四 zone flex(platform-padding · left-center · drag-flex · status)ProjectSwitcher.svelte:项目下拉 + grouped worktree 视图UpdateStatusPill.svelte:5 态机idle / available / downloading / downloaded / error+ 环形进度UpdatePopover.svelte:三按钮(立即更新 / 稍后提醒 / 跳过此版本)+ release notes + outside-click / Esc 关闭 + D3b idle race(idle 态时 popover 自动关)RosettaStatusIcon.svelte:黄三角 icon + tooltip + Shift-click 永久 dismiss- TabBar active indicator:用
box-shadow: inset 0 -2px 0不再 border-bottom 避免与行底 border 重影(D8)
- 内容组件:BaseItem、StatusDot、OutputBlock、SearchBar(Cmd+F)、CommandPalette(Cmd+K)、ContextPanel(Category/Ranked + DirectoryTree)、DiffViewer(LCS 行级 diff)、SessionContextMenu(右键 5 项)、DirectoryTree(递归目录树)、Tool Viewer(Read/Edit/Write/Bash/Default)。
- 图标:
ui/src/lib/icons.ts导出 lucide 风格 SVG path 常量,BaseItem 通过svgIconprop 渲染。
状态与主题
tabStore.svelte.ts管 tabs / activeTabId / per-tab UI 状态 / session 缓存 / notificationUnreadCount。sidebarStore.svelte.ts管 sidebar 宽度 + per-project Pin/Hide(内存级)。Settings / Notifications 状态各自组件内管。- 主题:
app.css:root浅色 +[data-theme="dark"]深色 +@media prefers-color-scheme跟随系统。lib/theme.ts::applyTheme()设置data-theme,App 启动从 config 读。
数据流(前端侧)
- Context Panel:后端
cdt-api→cdt-analyze::context::process_session_context_with_phases→ContextInjection[];CLAUDE.md 来源通过cdt-config::read_all_claude_md_files文件系统扫描。 - session 元数据:
list_sessionsIPC 与 HTTP 路径共用骨架 + push 实现(changeunify-session-list-loading-strategy)——返回骨架 SessionSummary(title=null/messageCount=0/isOngoing=false),后台JoinSet + Semaphore(8)并发扫描,每条通过subscribe_session_metadata()broadcast → Tauri emit / SSE/api/eventspush 同形的session-metadata-update→ Sidebar 按 sessionId in-place patch(不要替换 SessionSummary 实例引用,会触发整行 DOM 重建)。冷启视觉过渡由.metadata-pending静态 opacity 0.55 占位 +.session-title-text/.session-metatransition: opacity 150ms ease-outfade-in 承载(不用 shimmer 动画——DESIGN.md::The One Live Signal Rule边界条款 +PRODUCT.md::Design Principle 5双硬约束,changeremove-sidebar-metadata-shimmer撤销 PR #177 / #270 的 shimmer 引入路径并修补 fade-in 漏在子元素的旧 bug);切 project 来回时ui/src/lib/sessionListStore.svelte.ts提供 by-projectId LRU 缓存让 hydrate 立即生效。 - 通知:后端
mark_notification_read后app.emit("notification-update")/ 自动通知管线产新通知后app.emit("notification-added")推送;前端App.svelte::onMountsubscribeEvent双路监听 →refreshUnreadCount→ setUnreadCount +setBadgeCount。5 min 兜底轮询(notificationPollTimer)防 event 丢失:30 s 轮询会阻止 WKWebView idle / power nap → 反 perf.md "辅助工具 idle 稳态" 约束(issue #258)。 - file-change 节流链:后端
cdt-watch::FileWatcherdebounce 100 ms;前端ui/src/lib/fileChangeStore::dedupeRefresh仅合并 in-flight 期间的并发调用,不做时间节流。高频写 JSONL 会每几百 ms re-render——如需降频加 250 ms cooldown 或 trailing debounce。
Svelte 5 陷阱(high frequency 全部踩过)
$effect自动追踪 + props 顶层初始化要 untrack:$effect中读取的所有响应式变量自动成为依赖;模块顶层用 props 取初始值(如let uiState = getTabUIState(tabId)/const key = `session-detail-${tabId}`)也会触发state_referenced_locallywarning。两类场景都用import { untrack } from "svelte"+untrack(() => variable)。typical case:tabId 在组件生命周期内不变(切 tab 走 destroy/recreate),但 Svelte 5 仍要求显式声明。<button>嵌套禁止:浏览器会修复 DOM 结构导致 Svelte 假设失效。用<span role="button" tabindex="-1">替代。{@const}位置限制:只能是{#if}/{:else}/{#each}/{#snippet}/<Component>的直接子级,不能放在<div>等 HTML 元素内。在块开头集中声明。cache = source.field+cache ?? source.field兜底反模式:写完缓存 cache 永远不为 null,??fallback 永远不生效——source替换(props 实例换 / 父刷新)后 UI 卡在旧 cache 值。只在"真正派生出新值"时写 cache(如 IPC 返回新结构),其它分支让 cache 保持 null 让 fallback 自动消费实时 source。例:SubagentCard.svelte::ensureMessagesrollback / IPC 失败路径不写messagesLocal。codex 三轮 CR 重复发现。$effect订阅$derived派生值的去重:effect 只在派生值变化时重跑——派生内的依赖某 state 变了但派生输出(===)不变时,effect 不重跑。props 实例替换但派生指纹未变 → effect 不动 → 任何缓存了 props 字段的本地 state 会永久遮蔽 props 替换。检查清单:派生指纹包含所有"内容变化但 props ref 也可能变"的维度,或干脆不缓存让 props 字段直接派生消费。{@attach}挂副作用:DOM 元素需要副作用 + cleanup(ResizeObserver / IntersectionObserver / scroll listener 等)时用{@attach (el) => { ...setup; return () => cleanup; }},比bind:this + onMount + onDestroy三段式更内聚。例见Sidebar.svelte::session-list挂 ResizeObserver。- scoped CSS root attribute 必须
:global():写[data-theme="dark"] .my-class会被 svelte-check 报css_unused_selector——root html 的data-theme不在组件 scoped 范围。改:global([data-theme="dark"]) .my-class。 content-visibility: auto父级 throttle 子树 CSS animation:浏览器把离屏子树的 layout/paint/animation 跳过,spinner / ping / sweep / shimmer 离屏 + 回到视口会"半天才转一下"。修法:含持续 animation 的父容器 SHALL 退出 contain,如class:msg-row-contained={!hasAnimation}。已发:#121 OngoingBanner spinner / #122 OngoingBanner ping+sweep。
UI 组件规范(防回归)
- 下拉选择 SHALL 用
lib/components/Dropdown.svelte,禁止原生<select>:系统默认弹层会遮盖当前已选值(典型 macOS WKWebView),且跨平台样式不可控(chevron / 高度 / option hover 全是平台行为)。PR #128 当时正是修这个 bug 才引入Dropdown——PR #143 重写 Dashboard 时回退到原生 select 又踩了一次。SettingsView 风格用size="md"(默认),工具栏 / inline 紧凑用size="sm" minWidth={...}。已用位置可grep -rn 'from.*Dropdown.svelte' ui/src实时查。 - 搜索类 input SHALL 配齐 7 件套:
type="search"+autocomplete="off"+autocorrect="off"+autocapitalize="off"+spellcheck="false"+enterkeyhint="search"+aria-label,并加 CSS::-webkit-search-cancel-button { -webkit-appearance: none; display: none; }隐藏 WebKit 原生 clear 按钮。理由:macOS WKWebView 对小写字母自动弹「A ×」大写建议浮窗、浏览器 autocomplete 历史下拉、type=search自带 clear 按钮会与 / kbd 等装饰冲突。PR #138 统一处理;新加搜索框时复制 DashboardView.dash-search模板即可,已合规位置可grep -rn 'autocapitalize="off"' ui/src查。 - 竖向滚动容器 SHALL 配
scrollbar-gutter: stable(滚动条弹出压缩内容→横向跳变的 bug 已复发 3+ 次):内容随生命周期变化的加scrollbar-gutter: stable;;浮层/代码块首帧定型的加/* scrollbar-gutter-exempt: <原因> */豁免。src/lib/scrollbarGutter.guard.test.ts机械拦截(缺二者即 fail),完整 rationale 见该测试头注释。
Settings 与 config 修改
- 乐观更新模式:config 修改不能依赖
updateConfig返回值刷新 UI,应先乐观更新本地$state,异步调 API,失败时回滚(重新getConfig)。
键盘快捷键
行为契约:openspec/specs/keyboard-shortcuts/spec.md;模块:ui/src/lib/keyboard/ + ui/src/lib/platform.ts。
边界口诀:全局 mod-key 走 registry / 组件级 Escape/Enter/方向键各组件自管 / 多 instance shared shortcut 由同层 controller fanout(不在每 instance 重复注册)。详细 SHALL 句、pendingOverrides overlay、IPC fallback 等行为契约在 spec/design,不在此重述。
渲染依赖与高亮
- 依赖:
marked(markdown→HTML)+highlight.js(按需加载语言)+dompurify(XSS 防护)+mermaid(图表,动态 import)。highlight.js 不引入预制主题 CSS,用app.css自定义 Soft Charcoal token 颜色。 - hljs token 颜色单点维护:
.hljs-*token 颜色 SHALL 写在ui/src/app.css全局(与--syntax-*CSS 变量绑定,浅/深主题自动切换);组件内不要再写.<component> :global(.hljs-*) { color: ... }局部覆盖。历史散在 5 处导致 DiffViewer 漏写、Edit 工具行 +/- 背景下 token 无色。 getLanguageFromPath(ui/src/lib/toolHelpers.ts)优先级链路:精确特殊名(Dockerfile/Makefile)→ ext 真映射 → 前缀兜底(Dockerfile.dev)→ text;改动时不要破坏顺序,否则Jenkinsfile.kts(Kotlin DSL)会被错认 groovy。
列表 / 详情自动刷新反闪烁三原则
{#each}必须带稳定 key(AIChunk 用responses[0].uuid,UserChunk/System/Compact 用uuid,SessionSummary 用sessionId),否则 file-change 刷新时整段 DOM 重建 + mermaid/highlight.js 重跑。loadX(..., silent = false)加 silent 参数:file-change handler 传silent=true保留旧列表直到新数据到达,不要经过"加载中..."中间态。- 状态指示器嵌入已有 slot(如
<OngoingBanner>替代最后 AIChunk 的lastOutput,对齐原版LastOutputDisplay.tsx::isLastGroup && isSessionOngoing语义),不要作为独立节点追加到流尾部——显隐切换时 scrollHeight 跳变引发贴底滚动视觉抖动。
PaneView 与 SessionDetail
- PaneView
{#key}复合tabId@sessionId:单用activeTab.id在openOrReplaceTab(保留 tabId 仅换 sessionId)路径下不触发 SessionDetail destroy/recreate,导致详情页只换标题不刷新。复合 key 后旧实例 destroy 时SessionDetail.onDestroySHALL 用getTabSessionId(tabId) === sessionIdguard 防止把旧 session 的 expanded/scroll 状态写回 tabUIStates 污染新 session(tabId 不变 → 新实例直接读到旧 state)。
与原版对齐
- 前端文本清洗逻辑移植自
../claude-devtools/src/shared/utils/contentSanitizer.ts(sanitizeDisplayContent)。扩展 UI 功能时优先查原版src/renderer/和src/shared/对应实现,直接移植不要造轮子。 - 视觉规范 / 重写组件视觉级任务 SHALL 先 invoke
impeccableskill 拿 PRODUCT.md + DESIGN.md 设计禁令(side-stripe ban / hero-metric / glass / 渐变文字等)。
开发命令
pnpm --dir ui run check必须从项目根目录跑,从src-tauri/目录跑会找不到package.json。- 浏览器直接访问
127.0.0.1:5173会报invokeundefined——必须通过cargo tauri dev的窗口测试,或用pnpm --dir ui run dev+ 浏览器访问?mock=1&fixture=...(参见下文「浏览器调试入口」为何用127.0.0.1而非localhost)。 - worktree rebase 后若 origin/main 加新 ui 依赖(典型
tauri-plugin-opener),跑pnpm --dir ui install重装(pnpm hardlink + global store,lockfile 未变近瞬时;变了也只下差量)。
视觉改动自验
CSS / 布局 / 组件结构改动后,未打开浏览器验证过 = 未完成:
- 起 mock(见下文「浏览器调试入口」),每个状态分支(如 disabled / enabled / error)各截一张
- 自己 Read 截图过一遍:无逐字折行 / 列对齐 / 文案风格统一
- 看着不对优先
evaluate拿getBoundingClientRect+ computed style 定位,再修
vitest / svelte-check 抓不到 flex 撑垮、文字折行、文案不统一 —— 视觉只能视觉验。
浏览器调试入口
不开 Tauri 窗口调 UI:pnpm --dir ui run dev → http://127.0.0.1:5173/?mock=1&fixture=multi-project-rich。fixture 有 empty / single-project / multi-project-rich 三种,详见 ui/src/lib/__fixtures__/。仅 dev 启用,production bundle 完全不含 mockIPC(vite DCE 验证见 tauriMock.bundle.test.ts)。
为什么用 127.0.0.1 而非 localhost:vite.config.ts::server.host='127.0.0.1' 强 bind IPv4(与 Tauri WKWebView 解析 localhost 优先 IPv4 对齐,详 commit fix(dev): pin vite to 127.0.0.1:5173);浏览器在双栈环境可能优先解析 localhost → [::1] → 连不上。
测试基础设施陷阱
- vitest
globals: false是硬约束:设globals: true后 Playwright runner 报「test.describe was called in a file imported by configuration file」——vitest 通过 vite plugin 注入全局test/describe,污染 Playwright transform 链。ui/vitest.config.tsMUST 保持globals: false,vitest 测试显式import { test, describe, expect } from 'vitest'。 - production bundle DCE 整块包:消除 mockIPC chunk 必须
if (import.meta.env.DEV) { ... await import('./lib/tauriMock'); ... }整块包,不能用if (!DEV) return; ...早期 return——后者 vite 仍把 dynamic import chunk 输出到 dist。验证:rm -rf ui/dist && pnpm --dir ui run build && ls ui/dist/assets | grep -iE "mock|fixture"应空。 - bundle test 强制
NODE_ENV=production:tauriMock.bundle.test.ts调execSync('pnpm run build', { env: { ...process.env, NODE_ENV: 'production' } })——否则 vitest 父进程NODE_ENV=test传染给子进程,DCE 失效。默认 skip,本地手动用RUN_BUNDLE_TESTS=1 pnpm --dir ui run test:unit。 - vite optimizer cache 多 spec 跑后污染 Playwright:连续
pnpm exec playwright test后再跑可能报「test.describe in config」假错;rm -rf ui/node_modules/.vite ui/node_modules/.cache清掉即恢复。CI 上reuseExistingServer=false不受影响。 - Playwright 绕过 UI 直接调 store:sole pane +
pane.tabs.length === 0(即 Dashboard 工作台)时TabBar不渲染——空状态点不到任何 tab,且通知/设置已迁到UnifiedTitleBar。多 pane 时无论 tabs 是否为空仍渲染(承载 focus accent + drop zone)。main.tsdev-only 暴露window.__cdtTest = { openSettingsTab, openNotificationsTab, openTab, setActiveTab },spec 用page.evaluate(() => window.__cdtTest.openSettingsTab())绕过 sidebar virtualization 时序 flake。production bundle 由if (DEV)块 DCE,不暴露。 window.__cdtTest.openTab(sessionId, projectId, label)参数顺序:sessionId 在前、projectId 在后;反了会报Cannot read properties of undefined (reading 'length')。- vitest 测 svelte store 模块级
$state:模块级$state跨 vitest test 不 reset(每个 test 都拿同一个模块实例)。两种处理:(a) 渐进 assertion 不 reset(推荐);(b)vi.resetModules()+ dynamic import 强制重载(复杂)。 - Playwright
reuseExistingServer: true本地缓存 fixture 内存 state:fixture module 的fx.config等 mutable state 在 vite dev server 进程内存里——上一轮 e2e 改完 config 后下一轮跑会拿到串用 state。改 fixture 后 SHALLpkill -f vite强制 fresh webServer,或临时reuseExistingServer: false跑一遍。CI 上process.env.CI自动 fresh。 - mockIPC 注入
__TAURI_INTERNALS__,UI 代码不能用它判 mock vs real:tauriMock.ts::setupMockIPC内部的 mockWindows + mockIPC 会注入__TAURI_INTERNALS__,导致用if ('__TAURI_INTERNALS__' in window)做运行时分支的 UI 代码在?mock=1浏览器调试模式走错路径。架构原则:plugin 命令的 mock 走tauriMock.ts::buildHandler加 case(如'plugin:opener|open_url'→window.open),UI 代码统一调真 plugin API,IPC 层负责分流。
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

