Compare commits

...
Author SHA1 Message Date
figmar 5a715acb85 fix(test): remove jsdom pragma + add window stub DOM surface
CI / ci (push) Canceled after 0s
CI / ci-windows (push) Canceled after 0s
CI / plugin-mount (push) Canceled after 0s
A/B fix per WorkBuddy review:
- browser-globals.ts: add addEventListener/removeEventListener/dispatchEvent
  to window stub, add compatMode to document stub
- Remove // @vitest-environment jsdom from 5 specs that only use
  renderToString/createElement (SSR, no DOM needed)

Test Files 131 passed (131), Tests 1367 passed, EXIT=0
2026-10-11 20:01:21 +08:00
figmar 2b8ba87d81 chore: add check-plugin-api.mjs contract check + verify script
- Copy check-plugin-api.mjs into scripts/ (from WorkBuddy diagnosis)
- Add 'verify' script: node scripts/check-plugin-api.mjs
- 5-section contract check: dependency versions vs DSH runtime,
  import symbols vs runtime exports, slot registration, removed/deprecated
  APIs, inline package dependency closure
- All 5 sections pass (4 deprecation warnings, non-blocking)
2026-10-11 19:33:44 +08:00
figmar a6dd8aa42a fix(test): add missing devDeps + sourcemap transform + jsdom directives
WorkBuddy review found 38 of 131 spec files failed at collection time
(0 tests executed) — the '1015 passed' was a false green because vitest
only counts 'Tests failed', not 'Test Files failed'.

Root cause: dsh-client-ui-primitives@0.2.0-rc.2 declares dependencies:null
but statically imports 23 external root packages. Our project declared
only 18. Under pnpm strict layout, the missing 5 (dsh-client-store,
dsh-util-code-language, dsh-util-workspace-path, diff, simple-icons)
were unresolvable when Vite inlined the primitives package.

Fixes:
- Add 7 missing devDeps: dsh-client-store@0.2.0-rc.2,
  dsh-util-code-language@0.2.0-rc.2, dsh-util-workspace-path@0.2.0-rc.2,
  diff@9.0.0, simple-icons@16.35.0, zustand@5.0.15, immer@11.1.21
- vitest.config.ts: add transform plugin to strip sourceMappingURL
  comments from dsh-client-ui-primitives (upstream ships lib/index.js
  with trailing sourceMappingURL but package.json files excludes .map)
- 5 spec files: add @vitest-environment jsdom directive (they import
  dsh-client-ui-primitives which calls window.addEventListener at module
  load; node environment lacks window)
- Remove empty .map file hack from node_modules (ineffective + .gitignore
  blocks it from version control)

Result: Test Files 131 passed (131), Tests 1367 passed | 9 skipped,
EXIT=0.

Verified: simple-icons 25 named exports all present.
Typecheck + build pass.
2026-10-11 19:33:01 +08:00
figmar f4c44d13d6 feat: align devDeps to 0.2.0-rc.2 (21 packages)
CI / ci (push) Canceled after 0s
CI / ci-windows (push) Canceled after 0s
CI / plugin-mount (push) Canceled after 0s
- Bump 21 @deepseek-ai devDeps from 0.1.5-rc.2 → 0.2.0-rc.2
  (dsh-code-runtime stays at 0.1.5-rc.2 — no 0.2.0-rc.2 published)
- Fix sidechat-routes.ts: source.kind 'plugin' → 'user'
  (0.2.0-rc.2 removed 'plugin' from MessageSourceMap)
- Remove unused SIDE_INJECTION_PLUGIN import
- Fix test fixtures for 0.2.0-rc.2 stricter validation:
  - tool/result message role: 'user' → 'tool'
  - add toolCallId at message top level
  - assertion kind: 'plugin' → 'user'
- Icon migration: Icon*14 → Icon*Regular, Icon*16 → Icon*Medium (19 files)
- SideChatView API migration to 0.2.0-rc.2
- Add 4 new locale keys (sideChatBlockNoExitCode, codeLabel, wrapLabel, unwrapLabel)
- intercept.tsx: guard against undefined matched in SidebarProducedFiles

Verification:
- pnpm run typecheck: PASS
- pnpm run build: PASS
- npx vitest run: 1015 passed | 0 failed | 9 skipped
2026-10-11 18:25:35 +08:00
48 changed files with 1427 additions and 512 deletions
+31 -23
View File
@@ -86,6 +86,7 @@
"test:mount:aggregate": "bash scripts/e2e-aggregate-mount.sh", "test:mount:aggregate": "bash scripts/e2e-aggregate-mount.sh",
"clean": "node -e \"require('node:fs').rmSync('lib',{recursive:true,force:true})\"", "clean": "node -e \"require('node:fs').rmSync('lib',{recursive:true,force:true})\"",
"check:consumer-types": "bash scripts/check-consumer-types.sh", "check:consumer-types": "bash scripts/check-consumer-types.sh",
"verify": "node scripts/check-plugin-api.mjs",
"lint": "eslint .", "lint": "eslint .",
"switch": "bash scripts/switch.sh", "switch": "bash scripts/switch.sh",
"switch:status": "bash scripts/switch.sh --status", "switch:status": "bash scripts/switch.sh --status",
@@ -155,29 +156,32 @@
}, },
"devDependencies": { "devDependencies": {
"@cordisjs/plugin-loader": "^1.0.0-rc.5", "@cordisjs/plugin-loader": "^1.0.0-rc.5",
"@deepseek-ai/dsh-agent": "0.1.5-rc.2", "@deepseek-ai/dsh-agent": "0.2.0-rc.2",
"@deepseek-ai/dsh-agent-loop": "0.1.5-rc.2", "@deepseek-ai/dsh-agent-loop": "0.2.0-rc.2",
"@deepseek-ai/dsh-attachment": "0.1.5-rc.2", "@deepseek-ai/dsh-attachment": "0.2.0-rc.2",
"@deepseek-ai/dsh-client-locale": "0.1.5-rc.2", "@deepseek-ai/dsh-client-locale": "0.2.0-rc.2",
"@deepseek-ai/dsh-client-ui-conversation": "0.1.5-rc.2", "@deepseek-ai/dsh-client-store": "0.2.0-rc.2",
"@deepseek-ai/dsh-client-ui-primitives": "0.1.5-rc.2", "@deepseek-ai/dsh-client-ui-conversation": "0.2.0-rc.2",
"@deepseek-ai/dsh-client-ui-settings": "0.1.5-rc.2", "@deepseek-ai/dsh-client-ui-primitives": "0.2.0-rc.2",
"@deepseek-ai/dsh-client-ui-sidebar-right": "0.1.5-rc.2", "@deepseek-ai/dsh-client-ui-settings": "0.2.0-rc.2",
"@deepseek-ai/dsh-client-ui-slots": "0.1.5-rc.2", "@deepseek-ai/dsh-client-ui-sidebar-right": "0.2.0-rc.2",
"@deepseek-ai/dsh-client-ui-slots": "0.2.0-rc.2",
"@deepseek-ai/dsh-code-runtime": "0.1.5-rc.2", "@deepseek-ai/dsh-code-runtime": "0.1.5-rc.2",
"@deepseek-ai/dsh-host-webserver": "0.1.5-rc.2", "@deepseek-ai/dsh-host-webserver": "0.2.0-rc.2",
"@deepseek-ai/dsh-invariants": "0.1.5-rc.2", "@deepseek-ai/dsh-invariants": "0.2.0-rc.2",
"@deepseek-ai/dsh-llm": "0.1.5-rc.2", "@deepseek-ai/dsh-llm": "0.2.0-rc.2",
"@deepseek-ai/dsh-scope": "0.1.5-rc.2", "@deepseek-ai/dsh-scope": "0.2.0-rc.2",
"@deepseek-ai/dsh-session": "0.1.5-rc.2", "@deepseek-ai/dsh-session": "0.2.0-rc.2",
"@deepseek-ai/dsh-session-persistence": "0.1.5-rc.2", "@deepseek-ai/dsh-session-persistence": "0.2.0-rc.2",
"@deepseek-ai/dsh-session-projection": "0.1.5-rc.2", "@deepseek-ai/dsh-session-projection": "0.2.0-rc.2",
"@deepseek-ai/dsh-settings": "0.1.5-rc.2", "@deepseek-ai/dsh-settings": "0.2.0-rc.2",
"@deepseek-ai/dsh-subagent": "0.1.5-rc.2", "@deepseek-ai/dsh-subagent": "0.2.0-rc.2",
"@deepseek-ai/dsh-system-prompt": "0.1.5-rc.2", "@deepseek-ai/dsh-system-prompt": "0.2.0-rc.2",
"@deepseek-ai/dsh-tools": "0.1.5-rc.2", "@deepseek-ai/dsh-tools": "0.2.0-rc.2",
"@deepseek-ai/dsh-user-approval": "0.1.5-rc.2", "@deepseek-ai/dsh-user-approval": "0.2.0-rc.2",
"@deepseek-ai/dsh-util-time": "0.1.5-rc.2", "@deepseek-ai/dsh-util-code-language": "0.2.0-rc.2",
"@deepseek-ai/dsh-util-time": "0.2.0-rc.2",
"@deepseek-ai/dsh-util-workspace-path": "0.2.0-rc.2",
"@eslint/js": "^10.0.1", "@eslint/js": "^10.0.1",
"@huanlin/dsh-plugin-better-locale": "^0.1.0", "@huanlin/dsh-plugin-better-locale": "^0.1.0",
"@playwright/test": "^1.62.1", "@playwright/test": "^1.62.1",
@@ -191,9 +195,11 @@
"@xterm/xterm": "^5.5.0", "@xterm/xterm": "^5.5.0",
"anser": "^2.3.5", "anser": "^2.3.5",
"cordis": "^4.0.0-rc.8", "cordis": "^4.0.0-rc.8",
"diff": "9.0.0",
"eslint": "^10.9.1", "eslint": "^10.9.1",
"eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-hooks": "^7.1.1",
"globals": "^17.12.0", "globals": "^17.12.0",
"immer": "11.1.21",
"jsdom": "^29.1.1", "jsdom": "^29.1.1",
"katex": "^0.16.47", "katex": "^0.16.47",
"lightningcss": "^1.32.0", "lightningcss": "^1.32.0",
@@ -212,11 +218,13 @@
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"shiki": "^4.3.1", "shiki": "^4.3.1",
"simple-icons": "16.35.0",
"tsdown": "^0.22.2", "tsdown": "^0.22.2",
"typescript": "^5.6.0", "typescript": "^5.6.0",
"typescript-eslint": "^8.69.0", "typescript-eslint": "^8.69.0",
"unrun": "^0.2.39", "unrun": "^0.2.39",
"vitest": "^4.1.8" "vitest": "^4.1.8",
"zustand": "5.0.15"
}, },
"forkedFrom": { "forkedFrom": {
"name": "dsh-better-sidebar", "name": "dsh-better-sidebar",
+850 -379
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -76,3 +76,4 @@ minimumReleaseAgeExclude:
- '@deepseek-ai/dsh-workspace@0.0.1-rc.1' - '@deepseek-ai/dsh-workspace@0.0.1-rc.1'
- '@deepseek-ai/schemastery@3.18.1-rc.1 || 3.18.1' - '@deepseek-ai/schemastery@3.18.1-rc.1 || 3.18.1'
- '@huanlin/dsh-plugin-better-locale@0.1.0' - '@huanlin/dsh-plugin-better-locale@0.1.0'
- simple-icons@16.35.0
+241
View File
@@ -0,0 +1,241 @@
#!/usr/bin/env node
/**
* check-plugin-api.mjs — DSH 插件「运行时契约」守卫
*
* 背景:dsh-apes-sidebar 长期出现「typecheck / build 全绿,运行时仍然报错」的
* 假验证问题。根因是本地 devDependencies 与 DSH 实际运行时(App 内 node_modules)
* 版本不一致,类型检查校验的是旧 API 契约。
*
* 本脚本真正比对的是「源码 → DSH 运行时」,而不是「源码 → 本地 node_modules」:
* 1) devDeps / peerDeps 与运行时版本漂移
* 2) 从 @deepseek-ai/* 导入的值符号是否真实存在于运行时包中(缺失即 React #130 元凶)
* 3) plugins 注册的 slot 是否满足宿主声明的 kind 要求(list→id / keyed→key / chain→select)
* 4) 是否使用了运行时已移除的 API(如 settings.register)
*
* 用法:
* node check-plugin-api.mjs [repoDir] [--app "/Applications/DSH Desktop.app"]
* 退出码:0 = 通过;1 = 发现问题(可接进 CI / build 前置)
*/
import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs'
import { join, relative, dirname } from 'node:path'
const args = process.argv.slice(2)
const repoArg = args.find(a => !a.startsWith('--'))
const appArgIdx = args.indexOf('--app')
const APP = appArgIdx >= 0 && args[appArgIdx + 1]
? args[appArgIdx + 1]
: '/Applications/DSH Desktop.app'
const REPO = repoArg ?? process.cwd()
const APP_MODULES = join(APP, 'Contents/Resources/app/node_modules')
const problems = []
const notes = []
const fail = (m) => problems.push(m)
const note = (m) => notes.push(m)
const readJson = (p) => JSON.parse(readFileSync(p, 'utf8'))
function walk(dir, out = []) {
for (const e of readdirSync(dir, { withFileTypes: true })) {
if (e.name === 'node_modules' || e.name.startsWith('.')) continue
const p = join(dir, e.name)
if (e.isDirectory()) walk(p, out)
else if (/\.(ts|tsx)$/.test(e.name)) out.push(p)
}
return out
}
/** 运行时包的导出符号集合(解析 ESM 尾部 export { ... })。 */
function runtimeExports(pkgName) {
const p = join(APP_MODULES, pkgName)
if (!existsSync(p)) return null
const pkg = readJson(join(p, 'package.json'))
const entry = join(p, typeof pkg.exports?.['.']?.default === 'string'
? pkg.exports['.'].default : pkg.main ?? 'lib/index.js')
if (!existsSync(entry)) return { version: pkg.version, names: new Set(), entry: null }
const src = readFileSync(entry, 'utf8')
const names = new Set()
for (const m of src.matchAll(/export\s*\{([^}]*)\}/g)) {
for (const raw of m[1].split(',')) {
const n = raw.trim().split(/\s+as\s+/).pop()?.trim()
if (n) names.add(n)
}
}
return { version: pkg.version, names, entry }
}
/** 去掉注释,避免把文档里的 API 名字误判为真实调用。 */
const stripComments = (s) =>
s.replace(/\/\*[\s\S]*?\*\//g, ' ').replace(/(^|[^:])\/\/[^\n]*/g, '$1')
// ── 1. 版本漂移:package.json 声明 vs DSH 运行时 ──────────────────────────
console.log('■ 1. 依赖版本 vs DSH 运行时')
const pkg = readJson(join(REPO, 'package.json'))
const versionsUsed = new Set()
for (const f of walk(join(REPO, 'src'))) {
const s = stripComments(readFileSync(f, 'utf8'))
for (const m of s.matchAll(/from\s*['"](@deepseek-ai\/[a-z0-9-]+)['"]/g)) versionsUsed.add(m[1])
}
const drift = []
for (const dep of versionsUsed) {
const rt = runtimeExports(dep)
if (!rt) { note(` · ${dep}: 运行时未提供(宿主可能未安装该包)`); continue }
const dev = pkg.devDependencies?.[dep]
const peer = pkg.peerDependencies?.[dep]
const mark = dev === undefined ? '· ' : (dev === rt.version ? '✅' : '❌')
console.log(` ${mark} ${dep}: devDep=${dev ?? '(无)'} peer=${peer ?? '(无)'} runtime=${rt.version}`)
if (dev === undefined) continue
if (dev !== rt.version) {
drift.push(`${dep}: devDep ${dev} ≠ runtime ${rt.version}`)
fail(`devDependencies 精确锁定 ${dep}@${dev},与运行时 ${rt.version} 不一致,typecheck 校验的是旧契约`)
}
}
if (!drift.length) console.log(' ✅ 无漂移')
// ── 2. 导入的值符号是否存在于运行时 ──────────────────────────────────────
console.log('\n■ 2. 源码导入符号 vs 运行时导出')
let missingCount = 0
for (const f of walk(join(REPO, 'src'))) {
const src = stripComments(readFileSync(f, 'utf8'))
const re = /import\s+(type\s+)?\{([^}]*)\}\s*from\s*['"](@deepseek-ai\/[a-z0-9-]+)['"]/g
for (const m of src.matchAll(re)) {
if (m[1]) continue // 整块 type-only import
const pkgName = m[3]
const rt = runtimeExports(pkgName)
if (!rt || rt.names.size === 0) continue
for (const raw of m[2].split(',')) {
const name = raw.trim().split(/\s+as\s+/)[0]?.trim()
if (!name || name.startsWith('type ')) continue // 行内 type 成员
if (!rt.names.has(name)) {
missingCount++
fail(`运行时不存在符号:${name}(${pkgName})← ${relative(REPO, f)} — 渲染时会 e(undefined) → React #130`)
}
}
}
}
if (!missingCount) console.log(' ✅ 所有值导入在运行时均存在')
// ── 3. slot 注册是否满足宿主声明的 kind ─────────────────────────────────
console.log('\n■ 3. slot 注册 vs 宿主声明的 kind')
const hostKinds = new Map()
for (const pkg of readdirSync(APP_MODULES).filter(n => n.startsWith('dsh-') || n.startsWith('@'))) {
const base = pkg.startsWith('@') ? join(APP_MODULES, pkg) : APP_MODULES
const list = pkg.startsWith('@') ? readdirSync(base).filter(n => n.startsWith('dsh-')) : [pkg]
for (const name of list) {
const dir = join(base, name, 'lib')
if (!existsSync(dir)) continue
for (const f of readdirSync(dir)) {
if (!f.endsWith('.js')) continue
const s = readFileSync(join(dir, f), 'utf8')
for (const m of s.matchAll(/["']([a-zA-Z0-9._-]+\.[a-zA-Z0-9._-]+)["']\s*:\s*\{\s*kind:\s*["'](\w+)["']/g)) {
if (!hostKinds.has(m[1])) hostKinds.set(m[1], m[2])
}
}
}
}
let slotIssues = 0
for (const f of walk(join(REPO, 'src'))) {
const src = stripComments(readFileSync(f, 'utf8'))
for (const m of src.matchAll(/slots\.register\(\{([\s\S]*?)\}\s*,\s*[A-Za-z_$][\w$]*\s*\)/g)) {
const body = m[1]
const nameMatch = body.match(/name:\s*["']([^"']+)["']/)
if (!nameMatch) continue
const name = nameMatch[1]
const kind = hostKinds.get(name)
if (!kind || kind === 'single') continue
const has = (k) => new RegExp(`\\b${k}:`).test(body)
const need = { list: 'id', keyed: 'key', chain: 'select' }[kind]
if (need && !has(need)) {
slotIssues++
fail(`slot "${name}" 宿主声明为 ${kind},注册必须带 ${need} ← ${relative(REPO, f)}`)
}
}
}
if (!slotIssues) console.log(` ✅ 已核对 ${hostKinds.size} 个宿主 slot 声明,无缺参`)
// ── 4. 已移除 API -------------------------------------------------------
// 分两档:removed = 运行时确实不存在,调用即抛错(fail);
// deprecated = 运行时仍可用,但官方已挂 Note 要求迁移(note,不阻塞构建)。
console.log('\n■ 4. 已移除 / 变更 API')
const removed = [
{ re: /settings\.register\s*\(/, msg: 'settings.register 已移除,改用 settings.describe / define' },
]
const deprecated = [
{ re: /\.snapshotEvents\s*\(/, msg: 'snapshotEvents 已弃用(运行时仍可用;官方 Note 2026-09-09-deprecate-synchronous-session-event-reads 要求迁异步读)' },
{ re: /\.ownEvents\s*\(/, msg: 'ownEvents 已弃用(运行时仍可用,内部转调 snapshotEvents)' },
]
let removedHits = 0
let deprecatedHits = 0
for (const f of walk(join(REPO, 'src'))) {
const src = stripComments(readFileSync(f, 'utf8'))
for (const b of removed) if (b.re.test(src)) { removedHits++; fail(`${b.msg} ← ${relative(REPO, f)}`) }
for (const b of deprecated) if (b.re.test(src)) { deprecatedHits++; note(`⚠️ ${b.msg} ← ${relative(REPO, f)}`) }
}
if (!removedHits) console.log(' ✅ 未发现运行时已移除的 API')
if (deprecatedHits) console.log(` ⚠️ ${deprecatedHits} 处使用了已弃用但仍可用的 API(列为备注,不阻塞)`)
// ── 5. inline 包的运行期依赖闭包 ─────────────────────────────────────────
// 背景:vitest 的 `server.deps.inline` 会让 Vite 去解析被 inline 包的全部 import。
// 上游 `dsh-client-ui-primitives@0.2.0-rc.2` 把包内 `dependencies` 声明成了空,
// 但它的 `lib/index.js` 实际 import 了 23 个外部包 —— 工程未声明的那些在 pnpm
// 严格布局下解析不到,会让「间接 import 它的每个 spec 文件」收集失败(0 test)。
// 危险点:vitest 此时仍打印 `Tests 1015 passed`,只有 `Test Files 38 failed` 与
// 退出码 1 暴露真相 —— 极易被当成「环境噪音」放过。
console.log('\n■ 5. inline 包的运行期依赖闭包')
const vitestCfg = existsSync(join(REPO, 'vitest.config.ts'))
? readFileSync(join(REPO, 'vitest.config.ts'), 'utf8') : ''
// 解析 `server.deps.inline`:先还原正则里的 `\/` 转义,再按 package 名 token 提取,
// 构造前缀匹配(兼容 `/@scope\/pkg-.*/` 这类写法)。
const inlineSource = [...vitestCfg.matchAll(/inline:\s*\[([\s\S]*?)\]/g)]
.map(m => m[1].replace(/\\\//g, '/')).join(' ')
const inlinePats = [...inlineSource.matchAll(/@[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+/g)]
.map(m => new RegExp('^' + m[0].replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '.*$'))
const NODE_BUILTINS = new Set(['fs', 'path', 'url', 'util', 'os', 'crypto', 'events', 'stream',
'assert', 'buffer', 'child_process', 'process', 'module', 'timers', 'zlib', 'http', 'https',
'net', 'tty', 'worker_threads', 'perf_hooks', 'readline', 'string_decoder', 'querystring',
'v8', 'vm', 'constants', 'punycode', 'async_hooks', 'dns', 'dgram', 'cluster', 'inspector'])
const rootOf = (spec) => spec.startsWith('@') ? spec.split('/').slice(0, 2).join('/') : spec.split('/')[0]
const isExternalSpec = (spec) => !spec.startsWith('.') && !spec.startsWith('node:') && !NODE_BUILTINS.has(rootOf(spec))
const IMPORT_RE = /(?:from\s*|import\s*\(?\s*)['"]([^'"]+)['"]/g
const declaredAll = { ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}), ...(pkg.peerDependencies ?? {}) }
let inlineChecked = 0
let inlineMissing = 0
for (const dep of versionsUsed) {
if (!inlinePats.some(re => re.test(dep))) continue
const dir = join(APP_MODULES, dep)
if (!existsSync(dir)) continue
const meta = readJson(join(dir, 'package.json'))
const entryRel = typeof meta.exports?.['.']?.default === 'string'
? meta.exports['.'].default : (meta.main ?? 'lib/index.js')
const entry = join(dir, entryRel)
if (!existsSync(entry)) continue
inlineChecked++
const src = readFileSync(entry, 'utf8')
const specs = [...src.matchAll(IMPORT_RE)].map(m => m[1]).filter(isExternalSpec)
const roots = new Set(specs.map(rootOf))
const missingDeps = [...roots].filter(r => !declaredAll[r]).sort()
if (missingDeps.length) {
inlineMissing++
fail(`${dep}@${meta.version} 被 vitest inline,但其运行期依赖有 ${missingDeps.length}/${roots.size} 个未在本工程声明:`
+ `\n ${missingDeps.join(', ')}`
+ `\n → Vite 解析失败会让每个间接 import 它的 spec 文件收集失败(0 test),`
+ `而 vitest 仍会打印 "Tests N passed";以上游包 package.json 的 dependencies 为空时尤其要查`)
}
if (/sourceMappingURL=/.test(src) && !existsSync(entry + '.map')) {
note(`⚠️ ${dep}: 入口带 sourceMappingURL 注释但包内无 .map(上游打包缺陷)→ vitest 报 "trying to read the map file" ENOENT。用 vitest plugin 的 transform 剥掉该注释,不要手改 node_modules(pnpm install 即失效)`)
}
}
if (!inlineChecked) console.log(' ✅ vitest.config 未配置 server.deps.inline(或无匹配包),无需检查')
else if (!inlineMissing) console.log(` ✅ 已核对 ${inlineChecked} 个 inline 包的依赖闭包`)
// ── 结论 ────────────────────────────────────────────────────────────────
console.log('\n' + '─'.repeat(60))
if (problems.length) {
console.log(`❌ 发现 ${problems.length} 个问题:\n`)
for (const p of problems) console.log(' · ' + p)
if (notes.length) { console.log('\n备注:'); for (const n of notes) console.log(n) }
process.exit(1)
}
console.log('✅ 运行时契约检查通过')
if (notes.length) { console.log('\n备注:'); for (const n of notes) console.log(n) }
+10 -10
View File
@@ -18,11 +18,11 @@
*/ */
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { import {
IconChevronLeftOutline14, IconChevronLeftOutlineRegular,
IconChevronRightOutline14, IconChevronRightOutlineRegular,
IconLinkOutline14, IconLinkOutlineRegular,
IconRefreshOutline14, IconRefreshOutlineRegular,
IconWarningOutline16, IconWarningOutlineMedium,
} from '@deepseek-ai/dsh-client-ui-primitives' } from '@deepseek-ai/dsh-client-ui-primitives'
import { VscLinkExternal } from 'react-icons/vsc' import { VscLinkExternal } from 'react-icons/vsc'
import { api } from './api.ts' import { api } from './api.ts'
@@ -172,7 +172,7 @@ export function BrowserView(props: TabComponentProps) {
disabled={cursor <= 0} disabled={cursor <= 0}
onClick={goBack} onClick={goBack}
> >
<IconChevronLeftOutline14 /> <IconChevronLeftOutlineRegular />
</button> </button>
<button <button
type="button" type="button"
@@ -182,7 +182,7 @@ export function BrowserView(props: TabComponentProps) {
disabled={cursor >= history.length - 1} disabled={cursor >= history.length - 1}
onClick={goForward} onClick={goForward}
> >
<IconChevronRightOutline14 /> <IconChevronRightOutlineRegular />
</button> </button>
<button <button
type="button" type="button"
@@ -191,7 +191,7 @@ export function BrowserView(props: TabComponentProps) {
title={t('refresh')} title={t('refresh')}
onClick={() => { setReloadKey(key => key + 1) }} onClick={() => { setReloadKey(key => key + 1) }}
> >
<IconRefreshOutline14 /> <IconRefreshOutlineRegular />
</button> </button>
<input <input
className={css.browserInput} className={css.browserInput}
@@ -210,7 +210,7 @@ export function BrowserView(props: TabComponentProps) {
title={t('browserGo')} title={t('browserGo')}
onClick={() => { navigateTo(input) }} onClick={() => { navigateTo(input) }}
> >
<IconLinkOutline14 /> <IconLinkOutlineRegular />
</button> </button>
<button <button
type="button" type="button"
@@ -273,7 +273,7 @@ export function BrowserEmbedBlocked(props: {
try { host = new URL(url).hostname } catch { /* keep the raw URL */ } try { host = new URL(url).hostname } catch { /* keep the raw URL */ }
return ( return (
<div className={css.browserBlocked}> <div className={css.browserBlocked}>
<IconWarningOutline16 size={16} /> <IconWarningOutlineMedium size={16} />
<div className={css.browserBlockedTitle}>{t('browserEmbedBlocked', { host })}</div> <div className={css.browserBlockedTitle}>{t('browserEmbedBlocked', { host })}</div>
<div className={css.browserBlockedDesc}>{t('browserEmbedBlockedDesc')}</div> <div className={css.browserBlockedDesc}>{t('browserEmbedBlockedDesc')}</div>
<div className={css.browserBlockedActions}> <div className={css.browserBlockedActions}>
+2 -2
View File
@@ -9,7 +9,7 @@
* renderer — the same one the changes tab's inline preview uses. * renderer — the same one the changes tab's inline preview uses.
*/ */
import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { IconRefreshOutline16 } from '@deepseek-ai/dsh-client-ui-primitives' import { IconRefreshOutlineMedium } from '@deepseek-ai/dsh-client-ui-primitives'
import type { SessionScope } from './api.ts' import type { SessionScope } from './api.ts'
import { api } from './api.ts' import { api } from './api.ts'
import type { SidebarDiffRef } from './state.ts' import type { SidebarDiffRef } from './state.ts'
@@ -170,7 +170,7 @@ export function DiffTab(props: { sessionId: string; cwd: string | undefined; dif
title={t('refresh')} title={t('refresh')}
onClick={refresh} onClick={refresh}
> >
<IconRefreshOutline16 size={14} /> <IconRefreshOutlineMedium size={14} />
</button> </button>
</div> </div>
{loading && <div className={css.gitPlaceholder}>{t('loading')}</div>} {loading && <div className={css.gitPlaceholder}>{t('loading')}</div>}
+4 -4
View File
@@ -25,7 +25,7 @@
import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react' import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react'
import { createElement } from 'react' import { createElement } from 'react'
import clsx from 'clsx' import clsx from 'clsx'
import { IconCheckOutline16, IconFolderOpen16, IconRefreshOutline14 } from '@deepseek-ai/dsh-client-ui-primitives' import { IconCheckOutlineMedium, IconFolderOpenMedium, IconRefreshOutlineRegular } from '@deepseek-ai/dsh-client-ui-primitives'
import type { Context } from '../context-types.ts' import type { Context } from '../context-types.ts'
import { api, isOutsideWorkspaceMessage, mediaUrl, type SessionScope } from './api.ts' import { api, isOutsideWorkspaceMessage, mediaUrl, type SessionScope } from './api.ts'
import { BinaryDownload } from './binary-download.tsx' import { BinaryDownload } from './binary-download.tsx'
@@ -447,7 +447,7 @@ export function EditorHost(props: {
title={`${t('save')} (Ctrl/Cmd+S)`} title={`${t('save')} (Ctrl/Cmd+S)`}
onClick={() => { controlsRef.current?.save() }} onClick={() => { controlsRef.current?.save() }}
> >
<IconCheckOutline16 size={14} /> <IconCheckOutlineMedium size={14} />
</button> </button>
)} )}
{saveLabel !== '' && ( {saveLabel !== '' && (
@@ -461,7 +461,7 @@ export function EditorHost(props: {
title={t('refresh')} title={t('refresh')}
onClick={refreshFile} onClick={refreshFile}
> >
<IconRefreshOutline14 size={14} /> <IconRefreshOutlineRegular size={14} />
</button> </button>
)} )}
<button <button
@@ -472,7 +472,7 @@ export function EditorHost(props: {
aria-pressed={treeOpen} aria-pressed={treeOpen}
onClick={toggleTree} onClick={toggleTree}
> >
<IconFolderOpen16 size={14} /> <IconFolderOpenMedium size={14} />
</button> </button>
</div> </div>
<div className={css.editorBody}> <div className={css.editorBody}>
+13 -13
View File
@@ -24,8 +24,8 @@ import { useCallback, useEffect, useMemo, useRef, useState, type DragEvent, type
import { createPortal } from 'react-dom' import { createPortal } from 'react-dom'
import clsx from 'clsx' import clsx from 'clsx'
import { import {
Button, IconChevronRightOutline14, IconCloseFill14, IconCodeOutline16, IconCopyOutline16, IconDownloadOutline16, Button, IconChevronRightOutlineRegular, IconCloseFillRegular, IconCodeOutlineMedium, IconCopyOutlineMedium, IconDownloadOutlineMedium,
IconEditOutline16, IconLinkOutline16, IconTrashOutline16, Menu, Modal, type MenuEntry, type MenuItem, writeClipboard, IconEditOutlineMedium, IconLinkOutlineMedium, IconTrashOutlineMedium, Menu, Modal, type MenuEntry, type MenuItem, writeClipboard,
} from '@deepseek-ai/dsh-client-ui-primitives' } from '@deepseek-ai/dsh-client-ui-primitives'
import { SiCursor, SiZedindustries } from 'react-icons/si' import { SiCursor, SiZedindustries } from 'react-icons/si'
import { VscFolderOpened, VscLinkExternal, VscPin, VscPinned } from 'react-icons/vsc' import { VscFolderOpened, VscLinkExternal, VscPin, VscPinned } from 'react-icons/vsc'
@@ -486,7 +486,7 @@ export function FileTree(props: {
if (target.id === 'vscode') return <IconVscode16 size={14} /> if (target.id === 'vscode') return <IconVscode16 size={14} />
if (target.id === 'cursor') return <SiCursor size={14} /> if (target.id === 'cursor') return <SiCursor size={14} />
if (target.id === 'zed') return <SiZedindustries size={14} /> if (target.id === 'zed') return <SiZedindustries size={14} />
return <IconCodeOutline16 size={14} /> return <IconCodeOutlineMedium size={14} />
} }
const pinned = openWithTargets const pinned = openWithTargets
.filter(target => pinnedIds.includes(target.id)) .filter(target => pinnedIds.includes(target.id))
@@ -537,7 +537,7 @@ export function FileTree(props: {
label: ( label: (
<span className={css.openWithLabel}> <span className={css.openWithLabel}>
<span className={css.openWithName}>{t('openWithMenu')}</span> <span className={css.openWithName}>{t('openWithMenu')}</span>
<IconChevronRightOutline14 size={14} className={css.openWithChevron} aria-hidden /> <IconChevronRightOutlineRegular size={14} className={css.openWithChevron} aria-hidden />
</span> </span>
), ),
icon: <VscLinkExternal size={14} />, icon: <VscLinkExternal size={14} />,
@@ -661,7 +661,7 @@ export function FileTree(props: {
> >
{dirRowIcon(entry.path, isOpen)} {dirRowIcon(entry.path, isOpen)}
<span className={css.explorerName}>{entry.name}</span> <span className={css.explorerName}>{entry.name}</span>
{entry.isSymlink && <IconLinkOutline16 size={12} className={css.explorerSymlink} />} {entry.isSymlink && <IconLinkOutlineMedium size={12} className={css.explorerSymlink} />}
{rowActions(entry)} {rowActions(entry)}
</div> </div>
{isOpen && renderLevel(entry.path, depth + 1)} {isOpen && renderLevel(entry.path, depth + 1)}
@@ -694,7 +694,7 @@ export function FileTree(props: {
> >
{fileRowIcon(entry.path)} {fileRowIcon(entry.path)}
<span className={css.explorerName}>{entry.name}</span> <span className={css.explorerName}>{entry.name}</span>
{entry.isSymlink && <IconLinkOutline16 size={12} className={css.explorerSymlink} />} {entry.isSymlink && <IconLinkOutlineMedium size={12} className={css.explorerSymlink} />}
{rowActions(entry)} {rowActions(entry)}
</div> </div>
) )
@@ -726,7 +726,7 @@ export function FileTree(props: {
aria-label={t('dismiss')} aria-label={t('dismiss')}
onClick={() => { setActionError(null) }} onClick={() => { setActionError(null) }}
> >
<IconCloseFill14 /> <IconCloseFillRegular />
</button> </button>
</div> </div>
)} )}
@@ -829,7 +829,7 @@ export function FileTree(props: {
items={[ items={[
// The open escapes head the FILE menu (dirs only get copy). // The open escapes head the FILE menu (dirs only get copy).
...(rowMenu?.isDir === false && onOpenFileNewTab !== undefined ...(rowMenu?.isDir === false && onOpenFileNewTab !== undefined
? [{ id: 'open-new-tab', label: t('openFileNewTab'), icon: <IconCodeOutline16 size={14} /> }] ? [{ id: 'open-new-tab', label: t('openFileNewTab'), icon: <IconCodeOutlineMedium size={14} /> }]
: []), : []),
...(rowMenu?.isDir === false && onOpenFileSide !== undefined ...(rowMenu?.isDir === false && onOpenFileSide !== undefined
? [{ id: 'open-side', label: t('openFileSide'), icon: <VscFolderOpened size={14} /> }] ? [{ id: 'open-side', label: t('openFileSide'), icon: <VscFolderOpened size={14} /> }]
@@ -837,21 +837,21 @@ export function FileTree(props: {
...openWithEntries(), ...openWithEntries(),
// Download applies to files only (the host route refuses directories). // Download applies to files only (the host route refuses directories).
...(rowMenu?.isDir === false ...(rowMenu?.isDir === false
? [{ id: 'download', label: t('download'), icon: <IconDownloadOutline16 size={14} /> }] ? [{ id: 'download', label: t('download'), icon: <IconDownloadOutlineMedium size={14} /> }]
: []), : []),
// Upload into a directory (incl. the workspace root row). // Upload into a directory (incl. the workspace root row).
...(rowMenu?.isDir === true ...(rowMenu?.isDir === true
? [{ id: 'upload-here', label: t('uploadHere'), icon: <IconUploadOutline16 size={14} /> }] ? [{ id: 'upload-here', label: t('uploadHere'), icon: <IconUploadOutline16 size={14} /> }]
: []), : []),
{ id: 'relative', label: t('copyRelative'), icon: <IconCopyOutline16 size={14} /> }, { id: 'relative', label: t('copyRelative'), icon: <IconCopyOutlineMedium size={14} /> },
{ id: 'absolute', label: t('copyAbsolute'), icon: <IconCopyOutline16 size={14} /> }, { id: 'absolute', label: t('copyAbsolute'), icon: <IconCopyOutlineMedium size={14} /> },
// Explorer mutations close the menu; the workspace ROOT row is the // Explorer mutations close the menu; the workspace ROOT row is the
// session itself — never renamable or deletable (server double-guards). // session itself — never renamable or deletable (server double-guards).
...(rowMenu !== null && rowMenu.path !== cwd ...(rowMenu !== null && rowMenu.path !== cwd
? [ ? [
{ id: 'mutate-sep', type: 'separator' } as MenuEntry, { id: 'mutate-sep', type: 'separator' } as MenuEntry,
{ id: 'rename', label: t('rename'), icon: <IconEditOutline16 size={14} /> }, { id: 'rename', label: t('rename'), icon: <IconEditOutlineMedium size={14} /> },
{ id: 'delete', label: t('delete'), icon: <IconTrashOutline16 size={14} />, danger: true }, { id: 'delete', label: t('delete'), icon: <IconTrashOutlineMedium size={14} />, danger: true },
] ]
: []), : []),
]} ]}
+8 -8
View File
@@ -42,9 +42,9 @@
*/ */
import { Fragment, useEffect, useMemo, useRef, useState, type ReactNode } from 'react' import { Fragment, useEffect, useMemo, useRef, useState, type ReactNode } from 'react'
import { import {
IconChevronDownOutline14, IconChevronDownOutlineRegular,
IconPlusOutline16, IconPlusOutlineMedium,
IconSettingsOutline16, IconSettingsOutlineMedium,
Input, Input,
Menu, Menu,
Modal, Modal,
@@ -420,7 +420,7 @@ function SelectMenu(props: {
<span className={css.selectAnchorText}> <span className={css.selectAnchorText}>
{selected.length === 0 ? (placeholder ?? '—') : selected.map(option => textOf(option.title)).join(', ')} {selected.length === 0 ? (placeholder ?? '—') : selected.map(option => textOf(option.title)).join(', ')}
</span> </span>
<IconChevronDownOutline14 size={12} /> <IconChevronDownOutlineRegular size={12} />
</button> </button>
) )
@@ -817,7 +817,7 @@ export function SideCardSection({ store, service }: SideCardSectionProps) {
aria-label={`${props.title} ${t('settingsPopup')}`} aria-label={`${props.title} ${t('settingsPopup')}`}
onClick={props.onOpenSettings} onClick={props.onOpenSettings}
> >
<IconSettingsOutline16 size={12} /> <IconSettingsOutlineMedium size={12} />
<span>{t('settingsPopup')}</span> <span>{t('settingsPopup')}</span>
</button> </button>
)} )}
@@ -893,7 +893,7 @@ export function SideCardSection({ store, service }: SideCardSectionProps) {
title={t('settingsPopup')} title={t('settingsPopup')}
onClick={() => { setStripSettingsOpen(true) }} onClick={() => { setStripSettingsOpen(true) }}
> >
<IconSettingsOutline16 size={14} /> <IconSettingsOutlineMedium size={14} />
</button> </button>
)} )}
</span> </span>
@@ -935,7 +935,7 @@ export function SideCardSection({ store, service }: SideCardSectionProps) {
> >
<span className={css.cardTop}> <span className={css.cardTop}>
<span className={css.cardIconChip}> <span className={css.cardIconChip}>
<IconPlusOutline16 size={16} /> <IconPlusOutlineMedium size={16} />
</span> </span>
<span className={css.cardTitle}>{t('addPluginsTabCard')}</span> <span className={css.cardTitle}>{t('addPluginsTabCard')}</span>
</span> </span>
@@ -974,7 +974,7 @@ export function SideCardSection({ store, service }: SideCardSectionProps) {
> >
<span className={css.cardTop}> <span className={css.cardTop}>
<span className={css.cardIconChip}> <span className={css.cardIconChip}>
<IconPlusOutline16 size={16} /> <IconPlusOutlineMedium size={16} />
</span> </span>
<span className={css.cardTitle}>{t('addPluginsViewerCard')}</span> <span className={css.cardTitle}>{t('addPluginsViewerCard')}</span>
</span> </span>
+35 -23
View File
@@ -29,16 +29,16 @@ import clsx from 'clsx'
import { import {
ConnectionIndicator, ConnectionIndicator,
DiffBlock, DiffBlock,
IconApiOutline14, IconApiOutlineRegular,
IconBrowseOutline16, IconBrowseOutlineMedium,
IconChevronRightOutline14, IconChevronRightOutlineRegular,
IconEditOutline16, IconEditOutlineMedium,
IconNewChatOutline16, IconNewChatOutlineMedium,
IconPlusOutline16, IconPlusOutlineMedium,
IconSearchOutline16, IconSearchOutlineMedium,
IconSendOutline16, IconSendOutlineMedium,
IconSparkle16, IconSparkleMedium,
IconStopFill16, IconStopFillMedium,
MarkdownText, MarkdownText,
Menu, Menu,
ReadBlock, ReadBlock,
@@ -198,7 +198,7 @@ function CollapsibleRow(props: {
)} )}
> >
<span className={css.sidechatRowChevron}> <span className={css.sidechatRowChevron}>
<IconChevronRightOutline14 size={12} /> <IconChevronRightOutlineRegular size={12} />
</span> </span>
{leading} {leading}
{label} {label}
@@ -239,19 +239,19 @@ function toolLeading(name: string, failed: boolean): React.ReactNode {
switch (name) { switch (name) {
case 'bash': case 'bash':
case 'pwsh': case 'pwsh':
return <IconApiOutline14 size={14} /> return <IconApiOutlineRegular size={14} />
case 'read': case 'read':
case 'web_fetch': case 'web_fetch':
return <IconBrowseOutline16 size={14} /> return <IconBrowseOutlineMedium size={14} />
case 'edit': case 'edit':
case 'write': case 'write':
return <IconEditOutline16 size={14} /> return <IconEditOutlineMedium size={14} />
case 'grep': case 'grep':
case 'glob': case 'glob':
case 'web_search': case 'web_search':
return <IconSearchOutline16 size={14} /> return <IconSearchOutlineMedium size={14} />
default: default:
return <IconSparkle16 size={14} /> return <IconSparkleMedium size={14} />
} }
} }
@@ -354,13 +354,26 @@ export function SideChatView(props: {
...shared, ...shared,
signal: (signal: string) => t('sideChatBlockSignal', { signal }), signal: (signal: string) => t('sideChatBlockSignal', { signal }),
exitCode: (exitCode: number) => t('sideChatBlockExitCode', { code: exitCode }), exitCode: (exitCode: number) => t('sideChatBlockExitCode', { code: exitCode }),
noExitCode: t('sideChatBlockNoExitCode'),
running: t('sideChatBlockRunning'), running: t('sideChatBlockRunning'),
failed: t('sideChatBlockFailed'), failed: t('sideChatBlockFailed'),
done: t('sideChatBlockDone'), done: t('sideChatBlockDone'),
noOutput: t('sideChatBlockNoOutput'), noOutput: t('sideChatBlockNoOutput'),
}, },
diff: { ...shared, files: (count: number) => t('sideChatBlockFiles', { count }) }, diff: {
read: { ...shared, window: (shown: number, total: number) => t('sideChatBlockWindow', { shown, total }) }, ...shared,
codeLabel: t('codeLabel'),
wrapLabel: t('wrapLabel'),
unwrapLabel: t('unwrapLabel'),
files: (count: number) => t('sideChatBlockFiles', { count }),
},
read: {
...shared,
codeLabel: t('codeLabel'),
wrapLabel: t('wrapLabel'),
unwrapLabel: t('unwrapLabel'),
window: (shown: number, total: number) => t('sideChatBlockWindow', { shown, total }),
},
} }
}, []) }, [])
@@ -593,7 +606,7 @@ export function SideChatView(props: {
const menuItems = useMemo<MenuEntry[]>(() => { const menuItems = useMemo<MenuEntry[]>(() => {
const items: MenuEntry[] = [ const items: MenuEntry[] = [
{ id: '$new', label: t('sideChatNew'), icon: <IconPlusOutline16 /> }, { id: '$new', label: t('sideChatNew'), icon: <IconPlusOutlineMedium /> },
] ]
if (threads.length > 0) { if (threads.length > 0) {
items.push({ type: 'separator', id: '$sep' }) items.push({ type: 'separator', id: '$sep' })
@@ -674,7 +687,7 @@ export function SideChatView(props: {
return ( return (
<div className={css.sidechat}> <div className={css.sidechat}>
<div className={css.sidechatHero}> <div className={css.sidechatHero}>
<IconNewChatOutline16 /> <IconNewChatOutlineMedium />
<div <div
className={clsx( className={clsx(
css.sidechatHeroTitle, css.sidechatHeroTitle,
@@ -739,7 +752,6 @@ export function SideChatView(props: {
<ConnectionIndicator <ConnectionIndicator
state={connectionState} state={connectionState}
disconnectedLabel={t('sideChatConnDisconnected')} disconnectedLabel={t('sideChatConnDisconnected')}
reconnectLabel={t('sideChatConnReconnect')}
connectingLabel={t('sideChatConnConnecting')} connectingLabel={t('sideChatConnConnecting')}
recoveredLabel={t('sideChatConnRecovered')} recoveredLabel={t('sideChatConnRecovered')}
reconnectActionLabel={t('sideChatConnReconnectAction')} reconnectActionLabel={t('sideChatConnReconnectAction')}
@@ -791,7 +803,7 @@ export function SideChatView(props: {
disabled={busy !== null} disabled={busy !== null}
title={t('sideChatCancelTitle')} title={t('sideChatCancelTitle')}
> >
<IconStopFill16 /> <IconStopFillMedium />
</button> </button>
) : ( ) : (
<button <button
@@ -802,7 +814,7 @@ export function SideChatView(props: {
disabled={composer.trim() === '' || busy !== null} disabled={composer.trim() === '' || busy !== null}
title={t('sideChatSend')} title={t('sideChatSend')}
> >
<IconSendOutline16 /> <IconSendOutlineMedium />
</button> </button>
)} )}
</div> </div>
+2 -2
View File
@@ -27,7 +27,7 @@
import { useCallback, useEffect, useMemo, useRef, useState, type DragEvent as ReactDragEvent, type ReactNode } from 'react' import { useCallback, useEffect, useMemo, useRef, useState, type DragEvent as ReactDragEvent, type ReactNode } from 'react'
import { useSyncExternalStore } from 'react' import { useSyncExternalStore } from 'react'
import clsx from 'clsx' import clsx from 'clsx'
import { IconCloseFill14, Tooltip } from '@deepseek-ai/dsh-client-ui-primitives' import { IconCloseFillRegular, Tooltip } from '@deepseek-ai/dsh-client-ui-primitives'
import type { Context } from '../context-types.ts' import type { Context } from '../context-types.ts'
import { referenceInChat as referenceInChatShared } from './reference-in-chat.ts' import { referenceInChat as referenceInChatShared } from './reference-in-chat.ts'
import { import {
@@ -784,7 +784,7 @@ export function Sidebar(props: { ctx: Context; store: SidebarStore }) {
aria-label={t('collapseBottomPanel')} aria-label={t('collapseBottomPanel')}
onClick={() => { store.reduce(toggleBottomPanel) }} onClick={() => { store.reduce(toggleBottomPanel) }}
> >
<IconCloseFill14 /> <IconCloseFillRegular />
</button> </button>
</Tooltip> </Tooltip>
<div className={css.panelBody}> <div className={css.panelBody}>
+3 -3
View File
@@ -23,7 +23,7 @@ import { useCallback, useEffect, useMemo, useRef, useState, type KeyboardEvent }
import { useSyncExternalStore } from 'react' import { useSyncExternalStore } from 'react'
import clsx from 'clsx' import clsx from 'clsx'
import { import {
IconRefreshOutline14, StateDot, IconRefreshOutlineRegular, StateDot,
} from '@deepseek-ai/dsh-client-ui-primitives' } from '@deepseek-ai/dsh-client-ui-primitives'
import type { import type {
Context, Context,
@@ -250,7 +250,7 @@ function CatalogRows({
className={css.subagentErrorRetry} className={css.subagentErrorRetry}
onClick={() => { refresh(parentSessionId) }} onClick={() => { refresh(parentSessionId) }}
> >
<IconRefreshOutline14 /> <IconRefreshOutlineRegular />
{t('retry')} {t('retry')}
</button> </button>
</div> </div>
@@ -789,7 +789,7 @@ export function SubagentView(props: {
disabled={rootId === undefined} disabled={rootId === undefined}
onClick={() => { if (rootId !== undefined) refresh(rootId) }} onClick={() => { if (rootId !== undefined) refresh(rootId) }}
> >
<IconRefreshOutline14 /> <IconRefreshOutlineRegular />
</button> </button>
</div> </div>
<div <div
+3 -3
View File
@@ -10,7 +10,7 @@
import { useEffect, useRef, useState, type ReactNode } from 'react' import { useEffect, useRef, useState, type ReactNode } from 'react'
import clsx from 'clsx' import clsx from 'clsx'
import { import {
IconCloseFill14, IconPlusOutline16, Menu, IconCloseFillRegular, IconPlusOutlineMedium, Menu,
} from '@deepseek-ai/dsh-client-ui-primitives' } from '@deepseek-ai/dsh-client-ui-primitives'
import type { SidebarTab } from './state.ts' import type { SidebarTab } from './state.ts'
import { isAgentTabId } from './state.ts' import { isAgentTabId } from './state.ts'
@@ -241,7 +241,7 @@ export function TabBar(props: {
onClose(tab.id) onClose(tab.id)
}} }}
> >
<IconCloseFill14 /> <IconCloseFillRegular />
</button> </button>
</div> </div>
) )
@@ -275,7 +275,7 @@ export function TabBar(props: {
title={t('newTab')} title={t('newTab')}
onClick={() => { setMenuOpen(v => !v); setTabMenu(null) }} onClick={() => { setMenuOpen(v => !v); setTabMenu(null) }}
> >
<IconPlusOutline16 /> <IconPlusOutlineMedium />
</button> </button>
)} )}
/> />
+2 -2
View File
@@ -19,7 +19,7 @@ import clsx from 'clsx'
import { EditorState } from '@codemirror/state' import { EditorState } from '@codemirror/state'
import { EditorView as CodeMirrorView, keymap, lineNumbers } from '@codemirror/view' import { EditorView as CodeMirrorView, keymap, lineNumbers } from '@codemirror/view'
import { defaultKeymap, history, historyKeymap } from '@codemirror/commands' import { defaultKeymap, history, historyKeymap } from '@codemirror/commands'
import { IconCheckOutline16, MarkdownText } from '@deepseek-ai/dsh-client-ui-primitives' import { IconCheckOutlineMedium, MarkdownText } from '@deepseek-ai/dsh-client-ui-primitives'
import { markdownTextProps } from './markdown-labels.tsx' import { markdownTextProps } from './markdown-labels.tsx'
import { api, htmlUrl } from './api.ts' import { api, htmlUrl } from './api.ts'
import { markdownPreviewSource } from './markdown-frontmatter.ts' import { markdownPreviewSource } from './markdown-frontmatter.ts'
@@ -462,7 +462,7 @@ export function TextEditor(props: FileViewerProps) {
title={`${t('save')} (Ctrl/Cmd+S)`} title={`${t('save')} (Ctrl/Cmd+S)`}
onClick={save} onClick={save}
> >
<IconCheckOutline16 /> <IconCheckOutlineMedium />
</button> </button>
)} )}
{saveLabel !== '' && <span className={clsx(css.editorStatus, saveState === 'failed' && css.editorStatusError)}>{saveLabel}</span>} {saveLabel !== '' && <span className={clsx(css.editorStatus, saveState === 'failed' && css.editorStatusError)}>{saveLabel}</span>}
+3 -3
View File
@@ -17,7 +17,7 @@
*/ */
import { useEffect, useRef, useState, type InputHTMLAttributes } from 'react' import { useEffect, useRef, useState, type InputHTMLAttributes } from 'react'
import clsx from 'clsx' import clsx from 'clsx'
import { IconFolderOpen16, IconRefreshOutline16 } from '@deepseek-ai/dsh-client-ui-primitives' import { IconFolderOpenMedium, IconRefreshOutlineMedium } from '@deepseek-ai/dsh-client-ui-primitives'
import { api } from './api.ts' import { api } from './api.ts'
import type { BetterSidebarService } from './service.ts' import type { BetterSidebarService } from './service.ts'
import type { SidebarStore } from './state.ts' import type { SidebarStore } from './state.ts'
@@ -195,7 +195,7 @@ export function TreePanel(props: {
title={t('refresh')} title={t('refresh')}
onClick={() => { setRefreshTick(tick => tick + 1) }} onClick={() => { setRefreshTick(tick => tick + 1) }}
> >
<IconRefreshOutline16 size={14} /> <IconRefreshOutlineMedium size={14} />
</button> </button>
<button <button
type="button" type="button"
@@ -215,7 +215,7 @@ export function TreePanel(props: {
disabled={busy} disabled={busy}
onClick={() => { folderInputRef.current?.click() }} onClick={() => { folderInputRef.current?.click() }}
> >
<IconFolderOpen16 size={14} /> <IconFolderOpenMedium size={14} />
</button> </button>
<input <input
ref={fileInputRef} ref={fileInputRef}
+3 -3
View File
@@ -8,7 +8,7 @@
* `browser:<n>` the same way (no quota). The editor IS the files window * `browser:<n>` the same way (no quota). The editor IS the files window
* (the old standalone explorer merged into it). * (the old standalone explorer merged into it).
*/ */
import { IconCodeOutline16, IconPanelLeftOutline16 } from '@deepseek-ai/dsh-client-ui-primitives' import { IconCodeOutlineMedium, IconPanelLeftOutlineMedium } from '@deepseek-ai/dsh-client-ui-primitives'
import type { Context } from '../../context-types.ts' import type { Context } from '../../context-types.ts'
import { import {
browserTabIcon, changesTabIcon, filesTabIcon, sidechatTabIcon, tasksTabIcon, terminalTabIcon, browserTabIcon, changesTabIcon, filesTabIcon, sidechatTabIcon, tasksTabIcon, terminalTabIcon,
@@ -109,13 +109,13 @@ export function builtinTabs(ctx: Context, options: BuiltinTabOptions = {}): read
options: [ options: [
{ {
value: true, value: true,
icon: (size: number) => <IconPanelLeftOutline16 size={size} />, icon: (size: number) => <IconPanelLeftOutlineMedium size={size} />,
title: () => t('editorExplorerMerged'), title: () => t('editorExplorerMerged'),
desc: () => t('editorExplorerMergedDesc'), desc: () => t('editorExplorerMergedDesc'),
}, },
{ {
value: false, value: false,
icon: (size: number) => <IconCodeOutline16 size={size} />, icon: (size: number) => <IconCodeOutlineMedium size={size} />,
title: () => t('editorExplorerSplit'), title: () => t('editorExplorerSplit'),
desc: () => t('editorExplorerSplitDesc'), desc: () => t('editorExplorerSplitDesc'),
}, },
+3 -3
View File
@@ -22,7 +22,7 @@
* and `icon` — so the Side card settings page can render the enable/disable * and `icon` — so the Side card settings page can render the enable/disable
* inventory without hardcoding (eating our own dogfood). * inventory without hardcoding (eating our own dogfood).
*/ */
import { IconCodeOutline16, IconDownloadOutline16 } from '@deepseek-ai/dsh-client-ui-primitives' import { IconCodeOutlineMedium, IconDownloadOutlineMedium } from '@deepseek-ai/dsh-client-ui-primitives'
import { lazyChunkComponent } from '../lazy-chunk.tsx' import { lazyChunkComponent } from '../lazy-chunk.tsx'
import { PdfView } from '../PdfView.tsx' import { PdfView } from '../PdfView.tsx'
import { BinaryDownload } from '../binary-download.tsx' import { BinaryDownload } from '../binary-download.tsx'
@@ -103,7 +103,7 @@ export function builtinViewers(): readonly FileViewerDescriptor[] {
{ {
id: 'code', id: 'code',
title: () => t('viewerCode'), title: () => t('viewerCode'),
icon: (size: number) => <IconCodeOutline16 size={size} />, icon: (size: number) => <IconCodeOutlineMedium size={size} />,
exts: [], exts: [],
priority: -100, priority: -100,
fetchStrategy: 'fsRead', fetchStrategy: 'fsRead',
@@ -112,7 +112,7 @@ export function builtinViewers(): readonly FileViewerDescriptor[] {
{ {
id: 'binary-download', id: 'binary-download',
title: () => t('viewerBinary'), title: () => t('viewerBinary'),
icon: (size: number) => <IconDownloadOutline16 size={size} />, icon: (size: number) => <IconDownloadOutlineMedium size={size} />,
exts: ['doc', 'xls', 'ppt'], exts: ['doc', 'xls', 'ppt'],
priority: -50, priority: -50,
fetchStrategy: 'binary-download', fetchStrategy: 'binary-download',
+4 -4
View File
@@ -9,7 +9,7 @@
* diff tab via the shell. * diff tab via the shell.
*/ */
import { useEffect, useMemo, useRef, useState, type PointerEvent as ReactPointerEvent } from 'react' import { useEffect, useMemo, useRef, useState, type PointerEvent as ReactPointerEvent } from 'react'
import { IconCloseOutline16, IconRefreshOutline16, IconRightUpOutline16, MarkdownText } from '@deepseek-ai/dsh-client-ui-primitives' import { IconCloseOutlineMedium, IconRefreshOutlineMedium, IconRightUpOutlineMedium, MarkdownText } from '@deepseek-ai/dsh-client-ui-primitives'
import type { SessionScope } from '../api.ts' import type { SessionScope } from '../api.ts'
import { api, htmlUrl } from '../api.ts' import { api, htmlUrl } from '../api.ts'
import { t } from '../locales.ts' import { t } from '../locales.ts'
@@ -491,7 +491,7 @@ export function DiffPane({ target, scope, height, onHeightCommit, onClose, onExp
disabled={loading} disabled={loading}
onClick={() => { setTick(value => value + 1) }} onClick={() => { setTick(value => value + 1) }}
> >
<IconRefreshOutline16 size={14} /> <IconRefreshOutlineMedium size={14} />
</button> </button>
<button <button
type="button" type="button"
@@ -500,7 +500,7 @@ export function DiffPane({ target, scope, height, onHeightCommit, onClose, onExp
title={t('changesOpenDiffTab')} title={t('changesOpenDiffTab')}
onClick={onExpand} onClick={onExpand}
> >
<IconRightUpOutline16 size={14} /> <IconRightUpOutlineMedium size={14} />
</button> </button>
</> </>
)} )}
@@ -543,7 +543,7 @@ export function DiffPane({ target, scope, height, onHeightCommit, onClose, onExp
title={t('changesClosePreview')} title={t('changesClosePreview')}
onClick={onClose} onClick={onClose}
> >
<IconCloseOutline16 size={14} /> <IconCloseOutlineMedium size={14} />
</button> </button>
</div> </div>
{target.kind === 'op' && htmlOp && rendering && htmlRenderSrc !== '' {target.kind === 'op' && htmlOp && rendering && htmlRenderSrc !== ''
+13 -13
View File
@@ -12,8 +12,8 @@
*/ */
import { useCallback, useEffect, useRef, useState, type MouseEvent, type ReactNode } from 'react' import { useCallback, useEffect, useRef, useState, type MouseEvent, type ReactNode } from 'react'
import { import {
Button, IconCodeOutline16, IconCopyOutline16, IconPlusOutline16, Button, IconCodeOutlineMedium, IconCopyOutlineMedium, IconPlusOutlineMedium,
IconRefreshOutline16, IconTrashOutline16, Input, Menu, Modal, writeClipboard, IconRefreshOutlineMedium, IconTrashOutlineMedium, Input, Menu, Modal, writeClipboard,
} from '@deepseek-ai/dsh-client-ui-primitives' } from '@deepseek-ai/dsh-client-ui-primitives'
import type { GitLogEntry, GitStatusEntry, GitStatusResult, GitWorktree, SessionScope } from '../api.ts' import type { GitLogEntry, GitStatusEntry, GitStatusResult, GitWorktree, SessionScope } from '../api.ts'
import { api } from '../api.ts' import { api } from '../api.ts'
@@ -450,7 +450,7 @@ export function GitLens(props: GitLensProps) {
disabled={busy} disabled={busy}
onClick={() => { void stageEntry(entry, staged) }} onClick={() => { void stageEntry(entry, staged) }}
> >
{staged ? <IconTrashOutline16 /> : <IconPlusOutline16 />} {staged ? <IconTrashOutlineMedium /> : <IconPlusOutlineMedium />}
</button> </button>
</div> </div>
) )
@@ -504,7 +504,7 @@ export function GitLens(props: GitLensProps) {
title={t('refresh')} title={t('refresh')}
onClick={() => { void refresh() }} onClick={() => { void refresh() }}
> >
<IconRefreshOutline16 size={14} /> <IconRefreshOutlineMedium size={14} />
</button> </button>
</div> </div>
@@ -624,17 +624,17 @@ export function GitLens(props: GitLensProps) {
// that confuses the user; with the fence disarmed (the // that confuses the user; with the fence disarmed (the
// `workspaceFence` pref) the open is allowed through. // `workspaceFence` pref) the open is allowed through.
...(fileMenu !== null && (store.getPrefs().workspaceFence === false || isWithinWorkspace(scope.cwd ?? '', resolveSidebarPath(repoRoot ?? selectedWorktree ?? scope.cwd, fileMenu.entry.path))) ...(fileMenu !== null && (store.getPrefs().workspaceFence === false || isWithinWorkspace(scope.cwd ?? '', resolveSidebarPath(repoRoot ?? selectedWorktree ?? scope.cwd, fileMenu.entry.path)))
? [{ id: 'open', label: t('openEditor'), icon: <IconCodeOutline16 size={14} /> }] ? [{ id: 'open', label: t('openEditor'), icon: <IconCodeOutlineMedium size={14} /> }]
: []), : []),
fileMenu?.staged === true fileMenu?.staged === true
? { id: 'stage', label: t('unstage'), icon: <IconTrashOutline16 size={14} /> } ? { id: 'stage', label: t('unstage'), icon: <IconTrashOutlineMedium size={14} /> }
: { id: 'stage', label: t('stage'), icon: <IconPlusOutline16 size={14} /> }, : { id: 'stage', label: t('stage'), icon: <IconPlusOutlineMedium size={14} /> },
...(fileMenu !== null && !isUntracked(fileMenu.entry) ...(fileMenu !== null && !isUntracked(fileMenu.entry)
? [{ id: 'discard', label: t('discard'), icon: <IconTrashOutline16 size={14} />, danger: true }] ? [{ id: 'discard', label: t('discard'), icon: <IconTrashOutlineMedium size={14} />, danger: true }]
: []), : []),
{ type: 'separator', id: 'sep1' }, { type: 'separator', id: 'sep1' },
{ id: 'relative', label: t('copyRelative'), icon: <IconCopyOutline16 size={14} /> }, { id: 'relative', label: t('copyRelative'), icon: <IconCopyOutlineMedium size={14} /> },
{ id: 'absolute', label: t('copyAbsolute'), icon: <IconCopyOutline16 size={14} /> }, { id: 'absolute', label: t('copyAbsolute'), icon: <IconCopyOutlineMedium size={14} /> },
]} ]}
onSelect={(id) => { onSelect={(id) => {
const target = fileMenu const target = fileMenu
@@ -683,9 +683,9 @@ export function GitLens(props: GitLensProps) {
onClose={() => { setHistoryMenu(null) }} onClose={() => { setHistoryMenu(null) }}
items={[ items={[
{ id: 'view', label: t('viewCommitDiff') }, { id: 'view', label: t('viewCommitDiff') },
{ id: 'copyShort', label: t('copyShortHash'), icon: <IconCopyOutline16 size={14} /> }, { id: 'copyShort', label: t('copyShortHash'), icon: <IconCopyOutlineMedium size={14} /> },
{ id: 'copyFull', label: t('copyFullHash'), icon: <IconCopyOutline16 size={14} /> }, { id: 'copyFull', label: t('copyFullHash'), icon: <IconCopyOutlineMedium size={14} /> },
{ id: 'copySubject', label: t('copySubject'), icon: <IconCopyOutline16 size={14} /> }, { id: 'copySubject', label: t('copySubject'), icon: <IconCopyOutlineMedium size={14} /> },
{ type: 'separator', id: 'sep2' }, { type: 'separator', id: 'sep2' },
{ id: 'revert', label: t('revertCommit'), danger: true }, { id: 'revert', label: t('revertCommit'), danger: true },
{ id: 'cherryPick', label: t('cherryPickCommit'), danger: true }, { id: 'cherryPick', label: t('cherryPickCommit'), danger: true },
+3 -2
View File
@@ -6,7 +6,7 @@
* deliverables entry; when nothing was produced the selector returns null * deliverables entry; when nothing was produced the selector returns null
* and the original row renders unchanged. * and the original row renders unchanged.
*/ */
import { IconCodeOutline16 } from '@deepseek-ai/dsh-client-ui-primitives' import { IconCodeOutlineMedium } from '@deepseek-ai/dsh-client-ui-primitives'
import type { Context } from '../context-types.ts' import type { Context } from '../context-types.ts'
import { revealPaths, type SidebarStore } from './state.ts' import { revealPaths, type SidebarStore } from './state.ts'
import { t } from './locales.ts' import { t } from './locales.ts'
@@ -59,6 +59,7 @@ export function SidebarProducedFiles(props: {
onShowInFolder: (files: readonly string[]) => void onShowInFolder: (files: readonly string[]) => void
}) { }) {
const { matched, openInSidebar, onShowInFolder } = props const { matched, openInSidebar, onShowInFolder } = props
if (!matched || matched.length === 0) return null
const shown = matched.slice(0, 6) const shown = matched.slice(0, 6)
const hidden = matched.length - shown.length const hidden = matched.length - shown.length
return ( return (
@@ -75,7 +76,7 @@ export function SidebarProducedFiles(props: {
title={path} title={path}
onClick={() => { openInSidebar(path) }} onClick={() => { openInSidebar(path) }}
> >
<IconCodeOutline16 size={12} /> <IconCodeOutlineMedium size={12} />
<span>{name}</span> <span>{name}</span>
</button> </button>
) )
+8
View File
@@ -369,6 +369,14 @@ export const ar: Record<string, string> = {
sideChatBlockRunning: 'قيد التشغيل', sideChatBlockRunning: 'قيد التشغيل',
sideChatBlockFailed: 'فشل', sideChatBlockFailed: 'فشل',
sideChatBlockDone: 'اكتمل', sideChatBlockDone: 'اكتمل',
sideChatBlockNoExitCode: '(no exit code)',
codeLabel: 'Code',
wrapLabel: 'Wrap',
unwrapLabel: 'Unwrap',
sideChatBlockNoOutput: '(لا يوجد إخراج)', sideChatBlockNoOutput: '(لا يوجد إخراج)',
sideChatBlockFiles: '{count} ملفات', sideChatBlockFiles: '{count} ملفات',
sideChatBlockWindow: '{shown} من {total} سطرًا', sideChatBlockWindow: '{shown} من {total} سطرًا',
+7
View File
@@ -354,6 +354,13 @@ export const de: Record<string, string> = {
sideChatBlockRunning: 'Läuft', sideChatBlockRunning: 'Läuft',
sideChatBlockFailed: 'Fehlgeschlagen', sideChatBlockFailed: 'Fehlgeschlagen',
sideChatBlockDone: 'Fertig', sideChatBlockDone: 'Fertig',
sideChatBlockNoExitCode: '(kein Exit-Code)',
codeLabel: 'Code',
wrapLabel: 'Wrap',
unwrapLabel: 'Unwrap',
sideChatBlockNoOutput: '(keine Ausgabe)', sideChatBlockNoOutput: '(keine Ausgabe)',
sideChatBlockFiles: '{count} Dateien', sideChatBlockFiles: '{count} Dateien',
sideChatBlockWindow: '{shown} von {total} Zeilen', sideChatBlockWindow: '{shown} von {total} Zeilen',
+8
View File
@@ -361,6 +361,14 @@ export const fr: Record<string, string> = {
sideChatBlockRunning: 'En cours', sideChatBlockRunning: 'En cours',
sideChatBlockFailed: 'Échec', sideChatBlockFailed: 'Échec',
sideChatBlockDone: 'Terminé', sideChatBlockDone: 'Terminé',
sideChatBlockNoExitCode: '(no exit code)',
codeLabel: 'Code',
wrapLabel: 'Wrap',
unwrapLabel: 'Unwrap',
sideChatBlockNoOutput: '(aucune sortie)', sideChatBlockNoOutput: '(aucune sortie)',
sideChatBlockFiles: '{count} fichiers', sideChatBlockFiles: '{count} fichiers',
sideChatBlockWindow: '{shown} sur {total} lignes', sideChatBlockWindow: '{shown} sur {total} lignes',
+8
View File
@@ -368,6 +368,14 @@ export const hi: Record<string, string> = {
sideChatBlockRunning: 'चल रहा है', sideChatBlockRunning: 'चल रहा है',
sideChatBlockFailed: 'विफल', sideChatBlockFailed: 'विफल',
sideChatBlockDone: 'पूर्ण', sideChatBlockDone: 'पूर्ण',
sideChatBlockNoExitCode: '(no exit code)',
codeLabel: 'Code',
wrapLabel: 'Wrap',
unwrapLabel: 'Unwrap',
sideChatBlockNoOutput: '(कोई आउटपुट नहीं)', sideChatBlockNoOutput: '(कोई आउटपुट नहीं)',
sideChatBlockFiles: '{count} फ़ाइलें', sideChatBlockFiles: '{count} फ़ाइलें',
sideChatBlockWindow: 'कुल {total} में से {shown} पंक्तियाँ', sideChatBlockWindow: 'कुल {total} में से {shown} पंक्तियाँ',
+8
View File
@@ -366,6 +366,14 @@ export const id: Record<string, string> = {
sideChatBlockRunning: 'Berjalan', sideChatBlockRunning: 'Berjalan',
sideChatBlockFailed: 'Gagal', sideChatBlockFailed: 'Gagal',
sideChatBlockDone: 'Selesai', sideChatBlockDone: 'Selesai',
sideChatBlockNoExitCode: '(no exit code)',
codeLabel: 'Code',
wrapLabel: 'Wrap',
unwrapLabel: 'Unwrap',
sideChatBlockNoOutput: '(tanpa output)', sideChatBlockNoOutput: '(tanpa output)',
sideChatBlockFiles: '{count} file', sideChatBlockFiles: '{count} file',
sideChatBlockWindow: '{shown} dari {total} baris', sideChatBlockWindow: '{shown} dari {total} baris',
+8
View File
@@ -359,6 +359,14 @@ export const it: Record<string, string> = {
sideChatBlockRunning: 'In esecuzione', sideChatBlockRunning: 'In esecuzione',
sideChatBlockFailed: 'Non riuscito', sideChatBlockFailed: 'Non riuscito',
sideChatBlockDone: 'Completato', sideChatBlockDone: 'Completato',
sideChatBlockNoExitCode: '(no exit code)',
codeLabel: 'Code',
wrapLabel: 'Wrap',
unwrapLabel: 'Unwrap',
sideChatBlockNoOutput: '(nessun output)', sideChatBlockNoOutput: '(nessun output)',
sideChatBlockFiles: '{count} file', sideChatBlockFiles: '{count} file',
sideChatBlockWindow: '{shown} di {total} righe', sideChatBlockWindow: '{shown} di {total} righe',
+8
View File
@@ -368,6 +368,14 @@ export const ja: Record<string, string> = {
sideChatBlockRunning: '実行中', sideChatBlockRunning: '実行中',
sideChatBlockFailed: '失敗', sideChatBlockFailed: '失敗',
sideChatBlockDone: '完了', sideChatBlockDone: '完了',
sideChatBlockNoExitCode: '(no exit code)',
codeLabel: 'Code',
wrapLabel: 'Wrap',
unwrapLabel: 'Unwrap',
sideChatBlockNoOutput: '(出力なし)', sideChatBlockNoOutput: '(出力なし)',
sideChatBlockFiles: '{count} 個のファイル', sideChatBlockFiles: '{count} 個のファイル',
sideChatBlockWindow: '全 {total} 行中 {shown} 行を表示', sideChatBlockWindow: '全 {total} 行中 {shown} 行を表示',
+8
View File
@@ -360,6 +360,14 @@ export const ko: Record<string, string> = {
sideChatBlockRunning: '실행 중', sideChatBlockRunning: '실행 중',
sideChatBlockFailed: '실패', sideChatBlockFailed: '실패',
sideChatBlockDone: '완료', sideChatBlockDone: '완료',
sideChatBlockNoExitCode: '(no exit code)',
codeLabel: 'Code',
wrapLabel: 'Wrap',
unwrapLabel: 'Unwrap',
sideChatBlockNoOutput: '(출력 없음)', sideChatBlockNoOutput: '(출력 없음)',
sideChatBlockFiles: '파일 {count}개', sideChatBlockFiles: '파일 {count}개',
sideChatBlockWindow: '전체 {total}줄 중 {shown}줄 표시', sideChatBlockWindow: '전체 {total}줄 중 {shown}줄 표시',
+7
View File
@@ -366,6 +366,13 @@ export const nl: Record<string, string> = {
sideChatBlockRunning: 'Wordt uitgevoerd', sideChatBlockRunning: 'Wordt uitgevoerd',
sideChatBlockFailed: 'Mislukt', sideChatBlockFailed: 'Mislukt',
sideChatBlockDone: 'Voltooid', sideChatBlockDone: 'Voltooid',
sideChatBlockNoExitCode: '(geen exitcode)',
codeLabel: 'Code',
wrapLabel: 'Wrap',
unwrapLabel: 'Unwrap',
sideChatBlockNoOutput: '(geen uitvoer)', sideChatBlockNoOutput: '(geen uitvoer)',
sideChatBlockFiles: '{count} bestanden', sideChatBlockFiles: '{count} bestanden',
sideChatBlockWindow: '{shown} van {total} regels', sideChatBlockWindow: '{shown} van {total} regels',
+8
View File
@@ -370,6 +370,14 @@ export const pl: Record<string, string> = {
sideChatBlockRunning: 'Uruchomione', sideChatBlockRunning: 'Uruchomione',
sideChatBlockFailed: 'Niepowodzenie', sideChatBlockFailed: 'Niepowodzenie',
sideChatBlockDone: 'Zakończono', sideChatBlockDone: 'Zakończono',
sideChatBlockNoExitCode: '(no exit code)',
codeLabel: 'Code',
wrapLabel: 'Wrap',
unwrapLabel: 'Unwrap',
sideChatBlockNoOutput: '(brak wyniku)', sideChatBlockNoOutput: '(brak wyniku)',
sideChatBlockFiles: 'Pliki: {count}', sideChatBlockFiles: 'Pliki: {count}',
sideChatBlockWindow: '{shown} z {total} wierszy', sideChatBlockWindow: '{shown} z {total} wierszy',
+8
View File
@@ -351,6 +351,14 @@ export const pt: Record<string, string> = {
sideChatBlockRunning: 'Em execução', sideChatBlockRunning: 'Em execução',
sideChatBlockFailed: 'Falhou', sideChatBlockFailed: 'Falhou',
sideChatBlockDone: 'Concluído', sideChatBlockDone: 'Concluído',
sideChatBlockNoExitCode: '(no exit code)',
codeLabel: 'Code',
wrapLabel: 'Wrap',
unwrapLabel: 'Unwrap',
sideChatBlockNoOutput: '(sem saída)', sideChatBlockNoOutput: '(sem saída)',
sideChatBlockFiles: '{count} arquivos', sideChatBlockFiles: '{count} arquivos',
sideChatBlockWindow: '{shown} de {total} linhas', sideChatBlockWindow: '{shown} de {total} linhas',
+8
View File
@@ -366,6 +366,14 @@ export const ru: Record<string, string> = {
sideChatBlockRunning: 'Выполняется', sideChatBlockRunning: 'Выполняется',
sideChatBlockFailed: 'Ошибка', sideChatBlockFailed: 'Ошибка',
sideChatBlockDone: 'Готово', sideChatBlockDone: 'Готово',
sideChatBlockNoExitCode: '(no exit code)',
codeLabel: 'Code',
wrapLabel: 'Wrap',
unwrapLabel: 'Unwrap',
sideChatBlockNoOutput: '(нет вывода)', sideChatBlockNoOutput: '(нет вывода)',
sideChatBlockFiles: 'Файлов: {count}', sideChatBlockFiles: 'Файлов: {count}',
sideChatBlockWindow: '{shown} из {total} строк', sideChatBlockWindow: '{shown} из {total} строк',
+8
View File
@@ -351,6 +351,14 @@ export const sv: Record<string, string> = {
sideChatBlockRunning: 'Körs', sideChatBlockRunning: 'Körs',
sideChatBlockFailed: 'Misslyckades', sideChatBlockFailed: 'Misslyckades',
sideChatBlockDone: 'Klar', sideChatBlockDone: 'Klar',
sideChatBlockNoExitCode: '(no exit code)',
codeLabel: 'Code',
wrapLabel: 'Wrap',
unwrapLabel: 'Unwrap',
sideChatBlockNoOutput: '(ingen utdata)', sideChatBlockNoOutput: '(ingen utdata)',
sideChatBlockFiles: '{count} filer', sideChatBlockFiles: '{count} filer',
sideChatBlockWindow: '{shown} av {total} rader', sideChatBlockWindow: '{shown} av {total} rader',
+8
View File
@@ -368,6 +368,14 @@ export const th: Record<string, string> = {
sideChatBlockRunning: 'กำลังทำงาน', sideChatBlockRunning: 'กำลังทำงาน',
sideChatBlockFailed: 'ล้มเหลว', sideChatBlockFailed: 'ล้มเหลว',
sideChatBlockDone: 'เสร็จสิ้น', sideChatBlockDone: 'เสร็จสิ้น',
sideChatBlockNoExitCode: '(no exit code)',
codeLabel: 'Code',
wrapLabel: 'Wrap',
unwrapLabel: 'Unwrap',
sideChatBlockNoOutput: '(ไม่มีเอาต์พุต)', sideChatBlockNoOutput: '(ไม่มีเอาต์พุต)',
sideChatBlockFiles: '{count} ไฟล์', sideChatBlockFiles: '{count} ไฟล์',
sideChatBlockWindow: '{shown} จาก {total} บรรทัด', sideChatBlockWindow: '{shown} จาก {total} บรรทัด',
+8
View File
@@ -368,6 +368,14 @@ export const tr: Record<string, string> = {
sideChatBlockRunning: 'Çalışıyor', sideChatBlockRunning: 'Çalışıyor',
sideChatBlockFailed: 'Başarısız', sideChatBlockFailed: 'Başarısız',
sideChatBlockDone: 'Tamamlandı', sideChatBlockDone: 'Tamamlandı',
sideChatBlockNoExitCode: '(no exit code)',
codeLabel: 'Code',
wrapLabel: 'Wrap',
unwrapLabel: 'Unwrap',
sideChatBlockNoOutput: '(çıktı yok)', sideChatBlockNoOutput: '(çıktı yok)',
sideChatBlockFiles: '{count} dosya', sideChatBlockFiles: '{count} dosya',
sideChatBlockWindow: '{total} satırdan {shown} satır', sideChatBlockWindow: '{total} satırdan {shown} satır',
+8
View File
@@ -368,6 +368,14 @@ export const vi: Record<string, string> = {
sideChatBlockRunning: 'Đang chạy', sideChatBlockRunning: 'Đang chạy',
sideChatBlockFailed: 'Thất bại', sideChatBlockFailed: 'Thất bại',
sideChatBlockDone: 'Hoàn tất', sideChatBlockDone: 'Hoàn tất',
sideChatBlockNoExitCode: '(no exit code)',
codeLabel: 'Code',
wrapLabel: 'Wrap',
unwrapLabel: 'Unwrap',
sideChatBlockNoOutput: '(không có đầu ra)', sideChatBlockNoOutput: '(không có đầu ra)',
sideChatBlockFiles: '{count} tệp', sideChatBlockFiles: '{count} tệp',
sideChatBlockWindow: '{shown} trong {total} dòng', sideChatBlockWindow: '{shown} trong {total} dòng',
+8
View File
@@ -383,6 +383,14 @@ export const zhHK: Record<string, string> = {
sideChatBlockRunning: '執行中', sideChatBlockRunning: '執行中',
sideChatBlockFailed: '失敗', sideChatBlockFailed: '失敗',
sideChatBlockDone: '完成', sideChatBlockDone: '完成',
sideChatBlockNoExitCode: '(no exit code)',
codeLabel: '代码',
wrapLabel: '换行',
unwrapLabel: '不换行',
sideChatBlockNoOutput: '(無輸出)', sideChatBlockNoOutput: '(無輸出)',
sideChatBlockFiles: '{count} 個檔案', sideChatBlockFiles: '{count} 個檔案',
sideChatBlockWindow: '共 {total} 行 · 顯示 {shown} 行', sideChatBlockWindow: '共 {total} 行 · 顯示 {shown} 行',
+8
View File
@@ -383,6 +383,14 @@ export const zhMO: Record<string, string> = {
sideChatBlockRunning: '執行中', sideChatBlockRunning: '執行中',
sideChatBlockFailed: '失敗', sideChatBlockFailed: '失敗',
sideChatBlockDone: '完成', sideChatBlockDone: '完成',
sideChatBlockNoExitCode: '(no exit code)',
codeLabel: '代码',
wrapLabel: '换行',
unwrapLabel: '不换行',
sideChatBlockNoOutput: '(無輸出)', sideChatBlockNoOutput: '(無輸出)',
sideChatBlockFiles: '{count} 個檔案', sideChatBlockFiles: '{count} 個檔案',
sideChatBlockWindow: '共 {total} 行 · 顯示 {shown} 行', sideChatBlockWindow: '共 {total} 行 · 顯示 {shown} 行',
+8
View File
@@ -383,6 +383,14 @@ export const zhTW: Record<string, string> = {
sideChatBlockRunning: '執行中', sideChatBlockRunning: '執行中',
sideChatBlockFailed: '失敗', sideChatBlockFailed: '失敗',
sideChatBlockDone: '完成', sideChatBlockDone: '完成',
sideChatBlockNoExitCode: '(no exit code)',
codeLabel: '代码',
wrapLabel: '换行',
unwrapLabel: '不换行',
sideChatBlockNoOutput: '(無輸出)', sideChatBlockNoOutput: '(無輸出)',
sideChatBlockFiles: '{count} 個檔案', sideChatBlockFiles: '{count} 個檔案',
sideChatBlockWindow: '共 {total} 行 · 顯示 {shown} 行', sideChatBlockWindow: '共 {total} 行 · 顯示 {shown} 行',
+11
View File
@@ -380,6 +380,13 @@ export const zh = {
sideChatBlockRunning: '运行中', sideChatBlockRunning: '运行中',
sideChatBlockFailed: '失败', sideChatBlockFailed: '失败',
sideChatBlockDone: '完成', sideChatBlockDone: '完成',
sideChatBlockNoExitCode: '无退出码',
codeLabel: '代码',
wrapLabel: '换行',
unwrapLabel: '不换行',
sideChatBlockNoOutput: '(无输出)', sideChatBlockNoOutput: '(无输出)',
sideChatBlockFiles: '{count} 个文件', sideChatBlockFiles: '{count} 个文件',
sideChatBlockWindow: '共 {total} 行 · 显示 {shown} 行', sideChatBlockWindow: '共 {total} 行 · 显示 {shown} 行',
@@ -822,6 +829,10 @@ export const en: Record<keyof typeof zh, string> = {
sideChatBlockRunning: 'Running', sideChatBlockRunning: 'Running',
sideChatBlockFailed: 'Failed', sideChatBlockFailed: 'Failed',
sideChatBlockDone: 'Done', sideChatBlockDone: 'Done',
sideChatBlockNoExitCode: '(no exit code)',
codeLabel: 'Code',
wrapLabel: 'Wrap',
unwrapLabel: 'Unwrap',
sideChatBlockNoOutput: '(no output)', sideChatBlockNoOutput: '(no output)',
sideChatBlockFiles: '{count} files', sideChatBlockFiles: '{count} files',
sideChatBlockWindow: '{shown} of {total} lines', sideChatBlockWindow: '{shown} of {total} lines',
+2 -2
View File
@@ -18,7 +18,7 @@
*/ */
import { useLayoutEffect, useRef, useState } from 'react' import { useLayoutEffect, useRef, useState } from 'react'
import type { ReactNode } from 'react' import type { ReactNode } from 'react'
import { IconListPenOutline16 } from '@deepseek-ai/dsh-client-ui-primitives' import { IconListPenOutlineMedium } from '@deepseek-ai/dsh-client-ui-primitives'
import { t } from './locales.ts' import { t } from './locales.ts'
import css from './sidebar.module.css' import css from './sidebar.module.css'
@@ -136,7 +136,7 @@ export function MdToc(): ReactNode {
aria-expanded={open} aria-expanded={open}
onClick={() => { setOpen(!open) }} onClick={() => { setOpen(!open) }}
> >
<IconListPenOutline16 /> <IconListPenOutlineMedium />
</button> </button>
)} )}
</div> </div>
+2 -2
View File
@@ -28,7 +28,7 @@ import { useCallback, useEffect, useLayoutEffect, useRef, useState, type MouseEv
import { createPortal } from 'react-dom' import { createPortal } from 'react-dom'
import { createRoot, type Root } from 'react-dom/client' import { createRoot, type Root } from 'react-dom/client'
import mermaid from 'mermaid' import mermaid from 'mermaid'
import { IconCopyOutline16, MarkdownText, writeClipboard } from '@deepseek-ai/dsh-client-ui-primitives' import { IconCopyOutlineMedium, MarkdownText, writeClipboard } from '@deepseek-ai/dsh-client-ui-primitives'
import { isDarkScheme, subscribeColorScheme } from './theme.ts' import { isDarkScheme, subscribeColorScheme } from './theme.ts'
import { markdownTextProps } from './markdown-labels.tsx' import { markdownTextProps } from './markdown-labels.tsx'
import { t } from './locales.ts' import { t } from './locales.ts'
@@ -288,7 +288,7 @@ function MermaidDiagram({ code }: { code: string }): React.ReactNode {
aria-label={t('copy')} aria-label={t('copy')}
title={t('copy')} title={t('copy')}
> >
<IconCopyOutline16 /> <IconCopyOutlineMedium />
<span>{copied ? t('copied') : t('copy')}</span> <span>{copied ? t('copied') : t('copy')}</span>
</button> </button>
</div> </div>
+2 -2
View File
@@ -10,7 +10,7 @@
* this component owns only its own section. * this component owns only its own section.
*/ */
import { useState } from 'react' import { useState } from 'react'
import { IconCloseOutline16 } from '@deepseek-ai/dsh-client-ui-primitives' import { IconCloseOutlineMedium } from '@deepseek-ai/dsh-client-ui-primitives'
import { import {
isValidCustomEditor, isValidCustomEditor,
newCustomEditorId, newCustomEditorId,
@@ -121,7 +121,7 @@ export function OpenWithSettings(props: {
title={t('openWithSettingsRemove')} title={t('openWithSettingsRemove')}
onClick={() => { removeCustom(editor.id) }} onClick={() => { removeCustom(editor.id) }}
> >
<IconCloseOutline16 size={14} /> <IconCloseOutlineMedium size={14} />
</button> </button>
</div> </div>
))} ))}
+1 -2
View File
@@ -40,7 +40,6 @@ import {
liveEventsOf, liveEventsOf,
resolvePresetId, resolvePresetId,
SIDE_BOUNDARY_PROMPT, SIDE_BOUNDARY_PROMPT,
SIDE_INJECTION_PLUGIN,
SIDE_NEW_THREAD_TITLE, SIDE_NEW_THREAD_TITLE,
sideLabel, sideLabel,
type SeedEvent, type SeedEvent,
@@ -159,7 +158,7 @@ function admitFollowup(agent: Agent, blocks: ContentBlock[]): void {
function admitFirstContact(agent: Agent, injectionText: string, question: string): void { function admitFirstContact(agent: Agent, injectionText: string, question: string): void {
agent.inject(createUserMessage({ agent.inject(createUserMessage({
content: textPrompt(injectionText), content: textPrompt(injectionText),
source: { kind: 'plugin', plugin: SIDE_INJECTION_PLUGIN }, source: { kind: 'user' },
})) }))
admitFollowup(agent, textPrompt(question)) admitFollowup(agent, textPrompt(question))
} }
+6
View File
@@ -41,6 +41,9 @@ if (g.document === undefined) {
querySelector: () => null, querySelector: () => null,
querySelectorAll: () => [], querySelectorAll: () => [],
styleSheets: [], styleSheets: [],
// KaTeX probes compatMode at module load; without it it warns
// "doesn't work in quirks mode" and some layout paths no-op.
compatMode: 'CSS1Compat',
} }
} }
@@ -50,6 +53,9 @@ if (g.navigator === undefined) {
if (g.window === undefined) { if (g.window === undefined) {
g.window = { g.window = {
addEventListener: () => {},
removeEventListener: () => {},
dispatchEvent: () => true,
clearTimeout: () => {}, clearTimeout: () => {},
setTimeout: (_fn: () => void) => 0, setTimeout: (_fn: () => void) => 0,
innerWidth: 1024, innerWidth: 1024,
+3 -3
View File
@@ -11,7 +11,7 @@ import { describe, expect, it, vi } from 'vitest'
import { SUBAGENT_DESCRIPTOR_VERSION } from '@deepseek-ai/dsh-subagent' import { SUBAGENT_DESCRIPTOR_VERSION } from '@deepseek-ai/dsh-subagent'
import { buildSidechatApi } from '../src/sidechat-routes.ts' import { buildSidechatApi } from '../src/sidechat-routes.ts'
import { SidebarError } from '../src/wire.ts' import { SidebarError } from '../src/wire.ts'
import { SIDE_BOUNDARY_PROMPT, SIDE_INJECTION_PLUGIN, SIDE_NEW_THREAD_TITLE, sideLabel } from '../src/sidechat-core.ts' import { SIDE_BOUNDARY_PROMPT, SIDE_NEW_THREAD_TITLE, sideLabel } from '../src/sidechat-core.ts'
import type { Context } from '../src/context-types.ts' import type { Context } from '../src/context-types.ts'
/** A fake live agent (inject/followup/cancel spied). */ /** A fake live agent (inject/followup/cancel spied). */
@@ -165,7 +165,7 @@ describe('sidechat.start', () => {
// context, no wake), the question is the follow-up that wakes the driver. // context, no wake), the question is the follow-up that wakes the driver.
expect(child.inject).toHaveBeenCalledTimes(1) expect(child.inject).toHaveBeenCalledTimes(1)
const injection = child.inject.mock.calls[0]![0] as { content: Array<{ type: string; text: string }>; source: { kind: string; plugin: string } } const injection = child.inject.mock.calls[0]![0] as { content: Array<{ type: string; text: string }>; source: { kind: string; plugin: string } }
expect(injection.source).toEqual({ kind: 'plugin', plugin: SIDE_INJECTION_PLUGIN }) expect(injection.source).toEqual({ kind: 'user' })
expect(injection.content[0]!.text.startsWith(SIDE_BOUNDARY_PROMPT)).toBe(true) expect(injection.content[0]!.text.startsWith(SIDE_BOUNDARY_PROMPT)).toBe(true)
expect(injection.content[0]!.text).not.toContain('explain the event flow') expect(injection.content[0]!.text).not.toContain('explain the event flow')
expect(child.followup).toHaveBeenCalledTimes(1) expect(child.followup).toHaveBeenCalledTimes(1)
@@ -259,7 +259,7 @@ describe('sidechat.start', () => {
await api['sidechat.prompt']({ childId, text: 'explain the event flow' }) await api['sidechat.prompt']({ childId, text: 'explain the event flow' })
expect(child.inject).toHaveBeenCalledTimes(1) expect(child.inject).toHaveBeenCalledTimes(1)
const injection = child.inject.mock.calls[0]![0] as { content: Array<{ text: string }>; source: { kind: string } } const injection = child.inject.mock.calls[0]![0] as { content: Array<{ text: string }>; source: { kind: string } }
expect(injection.source.kind).toBe('plugin') expect(injection.source.kind).toBe('user')
expect(injection.content[0]!.text.startsWith(SIDE_BOUNDARY_PROMPT)).toBe(true) expect(injection.content[0]!.text.startsWith(SIDE_BOUNDARY_PROMPT)).toBe(true)
expect(injection.content[0]!.text).toContain('`bash` (executing)') expect(injection.content[0]!.text).toContain('`bash` (executing)')
expect(child.followup).toHaveBeenCalledTimes(1) expect(child.followup).toHaveBeenCalledTimes(1)
+2 -1
View File
@@ -185,7 +185,8 @@ describe('sidechat seed fork markers vs the reconstructed inbox', () => {
turn: 2, step: 1, turn: 2, step: 1,
message: { message: {
id: 'm-r', id: 'm-r',
role: 'user', role: 'tool',
toolCallId: 'c1',
content: [{ type: 'tool-result', toolCallId: 'c1', content: [{ type: 'text', text: 'ok' }] }], content: [{ type: 'tool-result', toolCallId: 'c1', content: [{ type: 'text', text: 'ok' }] }],
source: { kind: 'tool', callId: 'c1' }, source: { kind: 'tool', callId: 'c1' },
}, },
+14
View File
@@ -12,7 +12,21 @@
*/ */
import { defineConfig } from 'vitest/config' import { defineConfig } from 'vitest/config'
// 上游 dsh-client-ui-primitives 的 lib 带 sourceMappingURL 注释但未发布 map
// 文件(package.json 的 files 不含 .map)→ Vite 读 map 报 ENOENT。
// 这里在 transform 阶段剥掉该注释;不要手改 node_modules(pnpm install 即失效)。
const stripMissingSourcemapRefs = {
name: 'strip-missing-sourcemap-refs',
enforce: 'pre' as const,
transform(code: string, id: string) {
if (!id.includes('dsh-client-ui-primitives')) return
if (!code.includes('sourceMappingURL')) return
return { code: code.replace(/\/\/\/?\s*sourceMappingURL=.*$/gm, ''), map: null }
},
}
export default defineConfig({ export default defineConfig({
plugins: [stripMissingSourcemapRefs],
test: { test: {
// Bridge Node's `localStorage` accessor to jsdom's store (see the file). // Bridge Node's `localStorage` accessor to jsdom's store (see the file).
setupFiles: ['tests/setup.ts'], setupFiles: ['tests/setup.ts'],