From 15831e14e1598d7856562b3e63ed79c68373deeb Mon Sep 17 00:00:00 2001 From: wuxiangru915 Date: Fri, 14 Aug 2026 10:47:09 +0800 Subject: [PATCH 1/3] =?UTF-8?q?chore:=20=E6=98=BE=E5=BC=8F=E5=A3=B0?= =?UTF-8?q?=E6=98=8E=20unrun=20=E4=BE=9D=E8=B5=96=EF=BC=88tsdown=20?= =?UTF-8?q?=E7=9A=84=20optional=20peer=EF=BC=8C=E5=85=A8=E6=96=B0=E5=AE=89?= =?UTF-8?q?=E8=A3=85=E5=90=8E=E6=9E=84=E5=BB=BA=E5=BF=85=E9=9C=80=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 8 ++++++-- pnpm-lock.yaml | 22 ++++++++++++++++++++-- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 7e76dca..540dcab 100644 --- a/package.json +++ b/package.json @@ -104,7 +104,9 @@ "react-dom": "^18.2.0" }, "peerDependenciesMeta": { - "cordis": { "optional": true } + "cordis": { + "optional": true + } }, "dependencies": { "@aiden0z/pptx-renderer": "^1.2.4", @@ -169,6 +171,8 @@ "react-dom": "18.2.0", "tsdown": "^0.22.2", "typescript": "^5.6.0", + "unrun": "^0.3.1", "vitest": "^4.1.8" - } + }, + "packageManager": "pnpm@11.21.0+sha512.521705bce689924eac72f5a3587122f362689ef6571e55ba80076fd637c11132ecffada26fad4ea79c485bfddbfd3d5a2a5b05805a77e893de71ec8a6cca3bb1" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 496cfc5..45fd76f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -188,10 +188,13 @@ importers: version: 18.2.0(react@18.2.0) tsdown: specifier: ^0.22.2 - version: 0.22.14(typescript@5.6.2) + version: 0.22.14(typescript@5.6.2)(unrun@0.3.1) typescript: specifier: ^5.6.0 version: 5.6.2 + unrun: + specifier: ^0.3.1 + version: 0.3.1 vitest: specifier: ^4.1.8 version: 4.1.10(@types/node@24.13.3)(jsdom@29.1.1(@noble/hashes@2.2.0))(vite@8.2.1(@types/node@24.13.3)) @@ -3234,6 +3237,16 @@ packages: unist-util-visit@5.1.0: resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} + unrun@0.3.1: + resolution: {integrity: sha512-onIck/oNnCaytwths1ZVp1LK2Gq2hPoyFhiHebObuUXqR3S0uHuLLaBK8K6mRRgV7Ptip8AnNvaUsgzwWwBZuA==} + engines: {node: ^22.13.0 || >=24.0.0} + hasBin: true + peerDependencies: + synckit: ^0.11.11 + peerDependenciesMeta: + synckit: + optional: true + use-callback-ref@1.3.3: resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} engines: {node: '>=10'} @@ -7532,7 +7545,7 @@ snapshots: trim-lines@3.0.1: {} - tsdown@0.22.14(typescript@5.6.2): + tsdown@0.22.14(typescript@5.6.2)(unrun@0.3.1): dependencies: ansis: 4.3.1 cac: 7.0.0 @@ -7551,6 +7564,7 @@ snapshots: verkit: 0.3.2 optionalDependencies: typescript: 5.6.2 + unrun: 0.3.1 transitivePeerDependencies: - '@typescript/native-preview' - '@volar/typescript' @@ -7606,6 +7620,10 @@ snapshots: unist-util-is: 6.0.1 unist-util-visit-parents: 6.0.2 + unrun@0.3.1: + dependencies: + rolldown: 1.2.4 + use-callback-ref@1.3.3(@types/react@18.3.31)(react@18.2.0): dependencies: react: 18.2.0 From e55958879a62ce9af101724e817636927d81af3d Mon Sep 17 00:00:00 2001 From: wuxiangru915 Date: Fri, 14 Aug 2026 10:47:09 +0800 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20=E8=B5=84=E6=BA=90=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=99=A8=E6=94=AF=E6=8C=81=E6=8C=89=E6=A8=A1=E5=BC=8F?= =?UTF-8?q?=E6=8E=92=E9=99=A4=E6=96=87=E4=BB=B6/=E7=9B=AE=E5=BD=95?= =?UTF-8?q?=EF=BC=88issue=20#18=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 exclude-patterns 纯函数(单 * 通配、大小写不敏感、零依赖,预编译 matcher) - SidebarPrefs 新增 explorerExclude 字符串数组(缺省 [] = 不过滤,行为不变) - 声明式设置新增 texts 文本行(多行输入、失焦提交、去空/去重),explorer tab 声明排除模式 - ExplorerView 订阅 prefs,按模式过滤渲染,设置页修改即时生效 - 双语文案 + 单测(匹配逻辑 12、prefs 解析、设置行、组件渲染、schema 默认值) --- src/client/ExplorerView.tsx | 22 ++++- src/client/SideCardSection.module.css | 34 ++++++++ src/client/SideCardSection.tsx | 104 ++++++++++++++++++++-- src/client/builtins/tabs.tsx | 11 +++ src/client/locales.ts | 6 ++ src/client/prefs.ts | 18 ++++ src/client/service.ts | 21 +++++ src/config.ts | 3 + src/exclude-patterns.ts | 74 ++++++++++++++++ src/prefs-shared.ts | 8 ++ tests/builtins.spec.ts | 6 ++ tests/exclude-patterns.spec.ts | 89 +++++++++++++++++++ tests/explorer-exclude.spec.tsx | 121 ++++++++++++++++++++++++++ tests/plugin-shape.spec.ts | 2 +- tests/side-card-section.spec.tsx | 45 ++++++++-- tests/smoke.spec.ts | 2 + tests/unit.spec.ts | 24 ++++- 17 files changed, 569 insertions(+), 21 deletions(-) create mode 100644 src/exclude-patterns.ts create mode 100644 tests/exclude-patterns.spec.ts create mode 100644 tests/explorer-exclude.spec.tsx diff --git a/src/client/ExplorerView.tsx b/src/client/ExplorerView.tsx index d0eb593..e04e794 100644 --- a/src/client/ExplorerView.tsx +++ b/src/client/ExplorerView.tsx @@ -10,15 +10,17 @@ * "copied" label replacing the button after a successful write); file rows * also offer a download action (the host serves raw bytes, binary-safe). */ -import { useCallback, useEffect, useRef, useState, type MouseEvent, type ReactNode } from 'react' +import { useCallback, useEffect, useMemo, useRef, useState, type MouseEvent, type ReactNode } from 'react' import clsx from 'clsx' import { IconCodeOutline16, IconCopyOutline16, IconDownloadOutline16, IconFolderClose16, IconFolderOpen16, IconRefreshOutline16, Menu, writeClipboard, } from '@deepseek-ai/dsh-client-ui-primitives' import { api, downloadUrl, type FsEntry } from './api.ts' +import { compileExcludePatterns } from '../exclude-patterns.ts' import { relativeTo } from './paths.ts' import { t } from './locales.ts' +import type { SidebarStore } from './state.ts' import css from './sidebar.module.css' interface LevelData { @@ -39,16 +41,28 @@ const COPIED_MS = 1200 export function ExplorerView(props: { sessionId: string cwd: string | undefined + /** The shared store: the explorer excludes read from `store.getPrefs()` + * and re-render live when the settings change (issue #18). */ + store: SidebarStore expanded: string[] onToggle: (path: string) => void onOpenFile: (path: string) => void /** Insert `@` into the composer draft. */ onReferenceFile: (path: string) => void }) { - const { sessionId, cwd, expanded, onToggle, onOpenFile, onReferenceFile } = props + const { sessionId, cwd, store, expanded, onToggle, onOpenFile, onReferenceFile } = props const [data, setData] = useState>({}) const dataRef = useRef(data) const [refreshTick, setRefreshTick] = useState(0) + // Live exclude patterns: follow the shared prefs so a settings-page edit + // hides/show rows without reopening or manual refresh. + const [exclude, setExclude] = useState(() => store.getPrefs().explorerExclude) + useEffect(() => store.subscribe(() => { + setExclude(store.getPrefs().explorerExclude) + }), [store]) + // Precompiled matcher: the regexes are built once per pattern list, not + // per row per render (large levels × many patterns stay cheap). + const excluded = useMemo(() => compileExcludePatterns(exclude), [exclude]) /** The row whose path was just copied ("copied" label replaces its button). */ const [copiedPath, setCopiedPath] = useState(null) /** Open context menu: the row path (and whether it is a directory) plus the cursor position. */ @@ -142,7 +156,9 @@ export function ExplorerView(props: { ) } const entries = level.entries ?? [] - return entries.map(entry => { + // Hide entries whose NAME matches an exclude pattern (directories and + // files alike, case-insensitive single-`*` wildcards — issue #18). + return entries.filter(entry => !excluded(entry.name)).map(entry => { if (entry.isDir) { const isOpen = expanded.includes(entry.path) return ( diff --git a/src/client/SideCardSection.module.css b/src/client/SideCardSection.module.css index 2517744..5c7888f 100644 --- a/src/client/SideCardSection.module.css +++ b/src/client/SideCardSection.module.css @@ -240,6 +240,40 @@ border-bottom: none; } +/* One declarative TEXT row: title/desc stacked above a multi-line input + (vertical layout, unlike the horizontal checkbox row). */ +.textRow { + display: flex; + flex-direction: column; + gap: 10px; + padding: 16px 0; + border-bottom: 1px solid var(--dsw-alias-border-l2); +} + +/* The multi-line pattern input: monospace, compact, DSH text tokens. */ +.textSetting { + width: 100%; + box-sizing: border-box; + padding: 8px 10px; + border: 1px solid var(--dsw-alias-border-l2); + border-radius: 6px; + background: var(--dsw-alias-bg-layer-1); + color: var(--dsw-alias-label-primary); + font-family: var(--ds-font-family-code, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace); + font-size: 12px; + line-height: 18px; + resize: vertical; +} + +.textSetting:focus-visible { + outline: 2px solid var(--dsw-alias-border-l4); + outline-offset: 1px; +} + +.textSetting::placeholder { + color: var(--dsw-alias-label-tertiary); +} + .control { flex: none; display: flex; diff --git a/src/client/SideCardSection.tsx b/src/client/SideCardSection.tsx index fb159ac..ea276cb 100644 --- a/src/client/SideCardSection.tsx +++ b/src/client/SideCardSection.tsx @@ -16,9 +16,10 @@ * A card's on/off state is its VISUAL STATE: enabled = highlighted (brand * border + tinted fill + a check badge pinned to the card's far right), * disabled = neutral and dimmed. Features that declare `settings.toggles` - * carry a gear corner button that opens a native Modal with the related - * settings as native checkbox rows (e.g. the Subagent page's "auto-open - * when a subagent appears", the Terminal page's model terminal tools). + * / `settings.texts` carry a gear corner button that opens a native Modal + * with the related settings as native checkbox rows (e.g. the Subagent + * page's "auto-open when a subagent appears") and/or multi-line text rows + * (e.g. the Explorer page's exclude patterns). * * Writes ride the plugin's own fenced settings route (the host calls the * settings seam in-process — the DSH settings RPC domain does not serve @@ -29,7 +30,7 @@ * shows the wire error inline — a broken settings surface never crashes the * shell. */ -import { Fragment, useEffect, useRef, useState, type ReactNode } from 'react' +import { Fragment, useEffect, useMemo, useRef, useState, type ReactNode } from 'react' import { IconCheckOutline16, IconCodeOutline16, @@ -55,6 +56,7 @@ import type { SidebarStore } from './state.ts' import type { BetterSidebarService, FileViewerDescriptor, + SidebarSettingText, SidebarSettingToggle, TabDescriptor, } from './service.ts' @@ -105,17 +107,89 @@ function prefBool(prefs: SidebarPrefs, key: string): boolean { return (prefs as unknown as Record)[key] === true } +/** Read one string-array pref by declarative key (missing/non-array = []). */ +function prefStrings(prefs: SidebarPrefs, key: string): string[] { + const value = (prefs as unknown as Record)[key] + return Array.isArray(value) ? value : [] +} + +/** + * One declarative TEXT row: a multi-line input (one value per line) that + * commits on blur — the explorer exclude patterns. Draft stays local while + * typing; the external value (prefs round-trip) re-syncs the draft only + * when the committed array actually changes, so in-progress edits are + * never clobbered by re-renders. + */ +export function TextSettingRow(props: { + text: SidebarSettingText + prefs: SidebarPrefs + onCommit: (text: SidebarSettingText, lines: string[]) => void +}) { + const { text, prefs, onCommit } = props + const title = textOf(text.title) + const value = prefStrings(prefs, text.key) + const serialized = useMemo(() => value.join('\n'), [value]) + const [draft, setDraft] = useState(serialized) + useEffect(() => { setDraft(serialized) }, [serialized]) + /** Normalize: trim, drop blanks, dedupe preserving order. */ + const normalized = (): string[] => { + const lines: string[] = [] + for (const line of draft.split('\n')) { + const trimmed = line.trim() + if (trimmed === '' || lines.includes(trimmed)) continue + lines.push(trimmed) + } + return lines + } + /** Commit only when the edit actually changed the patterns (blur on a + * pristine box writes nothing). */ + const commit = (): void => { + const lines = normalized() + if (lines.length === value.length && lines.every((line, i) => line === value[i])) return + onCommit(text, lines) + } + return ( +
+ + {title} + {textOf(text.desc) !== '' && {textOf(text.desc)}} + +