Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,9 @@ interface TabDescriptor {
/**
* 声明式设置(v0.4.1+):每个注册的 tab 都会在 Side card 设置页获得一行
* 开关(图标 + 标题 + 类型 id),`settings.toggles` 在其行下追加嵌套设置行,
* 绑定 SidebarPrefs 字段。嵌套设置仅父级启用时显示(v0.11.0 起行控件不限于
* `settings.texts` 追加多行文本输入行(每行一个值,失焦提交到
* SidebarPrefs 的字符串数组字段),均绑定 SidebarPrefs 字段。嵌套设置仅
* 父级启用时显示(齿轮按钮在父级卡片上)。v0.11.0 起行控件不限于
* 布尔开关:`type: 'switch' | 'text' | 'number'`,缺省 'switch';text/number
* 行 blur/Enter 提交,number 行按 min/max 钳制,unit 渲染单位后缀)。
* v0.12.0 起增加两个插件自有扩展(详见 §5 声明式设置):
Expand All @@ -171,6 +173,13 @@ interface TabDescriptor {
/** 输入框后的单位后缀(如 'px')。 */
unit?: string
}[]
texts?: readonly {
/** SidebarPrefs 字符串数组字段名(内置键:'explorerExclude') */
key: string
title: string | (() => string)
desc?: string | (() => string)
placeholder?: string | (() => string)
}[]
/** 插件自有设置行(v0.12.0+):形状同 toggles,但 key 是插件局部的,
* 持久化在 `pluginSettings[<descriptor id>]`——不需要宿主 PrefsSchema 字段。 */
pluginToggles?: readonly {
Expand Down Expand Up @@ -328,7 +337,7 @@ interface FileViewerDescriptor {
* 卸载/重匹配时中止(忽略 signal 的 load 也照常工作) */
load?: (path: string, scope: SessionScope, signal?: AbortSignal) => Promise<unknown>
/** 声明式设置(v0.4.1+):形状同 TabDescriptor.settings */
settings?: { toggles?: readonly { key: string; title: string | (() => string); desc?: string | (() => string) }[] }
settings?: { toggles?: readonly { key: string; title: string | (() => string); desc?: string | (() => string) }[]; texts?: readonly { key: string; title: string | (() => string); desc?: string | (() => string); placeholder?: string | (() => string) }[] }
/** 渲染函数 */
component: (props: FileViewerProps) => ReactNode
}
Expand Down Expand Up @@ -503,7 +512,7 @@ interface OpenTabSeed {
}
```

> **声明式设置(v0.4.1+)**:每个注册的 tab/viewer 自动出现在 DSH 设置页「侧边卡片」分区的清单里——响应式网格中的**小卡片**(图标 + 标题 + 类型 id + **高亮 = 启用**,勾选徽标钉在卡片最右端,viewer 卡片还显示扩展名),开关持久化到 `SidebarPrefs.tabsEnabled / viewersEnabled`(开放 map,缺省 = 启用)。关闭语义:tab 从 `+` 菜单消失、`openTab` 拒绝新开、子代理自动展开 / agent 终端自动补 tab 等派生流程停止,**已打开的 tab 保留**;viewer 被 `matchFileViewer` 跳过,文件落到下一个匹配。`settings.toggles` 声明的相关设置(如子代理的 `autoOpenSubagent`、终端的 `terminalFontFamily`/`terminalFontSize`)通过卡片右下角的齿轮按钮在**原生弹窗**中编辑——`type: 'switch'` 行是复选框,`type: 'text'`/`'number'` 行是输入框(v0.11.0+)——父级卡片关闭时齿轮隐藏;`settings.toggles` 的 **key 必须是宿主 PrefsSchema 的字段**(内置键:`autoOpenSubagent` / `agentTerminalTools` / `terminalFontFamily` / `terminalFontSize` / `htmlViewerNoSandbox` / `htmlViewerDefaultUnsafe` / `browserNoSandbox` / `browserInterceptLinks`)。**v0.12.0 起设置 seam 已开放**:外部插件用 `settings.pluginToggles`(同款行控件,key 插件局部)或 `settings.render`(自定义面板)声明自己的设置,值持久化在 prefs 文档的 `pluginSettings[<descriptor id>]`(开放 map,宿主 schema 已有字段,无需注册)——齿轮弹窗对 tab 与 viewer 都可用(viewer 卡片 v0.12.0 起也有齿轮)。
> **声明式设置(v0.4.1+)**:每个注册的 tab/viewer 自动出现在 DSH 设置页「侧边卡片」分区的清单里——响应式网格中的**小卡片**(图标 + 标题 + 类型 id + **高亮 = 启用**,勾选徽标钉在卡片最右端,viewer 卡片还显示扩展名),开关持久化到 `SidebarPrefs.tabsEnabled / viewersEnabled`(开放 map,缺省 = 启用)。关闭语义:tab 从 `+` 菜单消失、`openTab` 拒绝新开、子代理自动展开 / agent 终端自动补 tab 等派生流程停止,**已打开的 tab 保留**;viewer 被 `matchFileViewer` 跳过,文件落到下一个匹配。`settings.toggles` 声明的相关设置(如子代理的 `autoOpenSubagent`、终端的 `terminalFontFamily`/`terminalFontSize`)通过卡片右下角的齿轮按钮在**原生弹窗**中编辑——`type: 'switch'` 行是复选框,`type: 'text'`/`'number'` 行是输入框(v0.11.0+);`settings.texts` 声明的文本设置(如资源管理器的 `explorerExclude`)在同一弹窗中渲染**多行文本输入**(每行一个值,失焦提交到 SidebarPrefs 的字符串数组字段)——父级卡片关闭时齿轮隐藏;`settings.toggles` / `settings.texts` 的 **key 必须是宿主 PrefsSchema 的字段**(内置 toggles 键:`autoOpenSubagent` / `agentTerminalTools` / `terminalFontFamily` / `terminalFontSize` / `htmlViewerNoSandbox` / `htmlViewerDefaultUnsafe` / `browserNoSandbox` / `browserInterceptLinks`;内置 texts 键:`explorerExclude`)。**v0.12.0 起设置 seam 已开放**:外部插件用 `settings.pluginToggles`(同款行控件,key 插件局部)或 `settings.render`(自定义面板)声明自己的设置,值持久化在 prefs 文档的 `pluginSettings[<descriptor id>]`(开放 map,宿主 schema 已有字段,无需注册)——齿轮弹窗对 tab 与 viewer 都可用(viewer 卡片 v0.12.0 起也有齿轮)。

---

Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

## ✨ 功能一览

- **🗂️ 文件工作台**:资源管理器(懒加载目录树)+ CodeMirror 编辑器;图片 / Markdown / HTML / PDF / Office 内联预览
- **🗂️ 文件工作台**:资源管理器(懒加载目录树,支持按模式排除文件/目录——设置页「排除模式」可配,如 `*.meta`、`node_modules`,单个 `*` 通配、大小写不敏感)+ CodeMirror 编辑器;图片 / Markdown / HTML / PDF / Office 内联预览
- **🌐 内嵌浏览器**:多开网页 tab,后退 / 前进 / 刷新;内容运行在沙箱 iframe,外链默认在侧边栏打开
- **💻 真实终端**:xterm.js + node-pty 真实 shell,断线重连回放;可选为模型注入 `terminal_*` 工具
- **🌿 Git 面板**:真 diff + VSCode 式 diff tab、历史、右键暂存 / 提交 / 还原
Expand Down Expand Up @@ -251,6 +251,7 @@ pnpm watch # tsdown --watch
## ⚠️ 已知限制

- Git 无 push/pull/fetch;无文件 watcher(手动刷新);工具行内文件打开按钮不可拦截
- 资源管理器排除是显示层过滤:超大目录(单层 >1000 项)的截断计数在过滤后可能偏少
- 终端 Tab 拖到另一分栏会重挂载(shell 重开)
- Office 三件套预览(.docx/.xlsx/.pptx)已移至「推荐插件」(Office 预览插件,见设置页「添加插件」弹窗);未安装时此类文件走代码/下载查看兜底
- 浏览器沙箱无登录态/第三方 Cookie 受限,部分站点登录需走弹窗;被 `X-Frame-Options`/`frame-ancestors` 拒绝嵌入的站点(如 arxiv.org)显示原因面板(含「在浏览器中打开」);iframe 内部跳转不进后退栈
Expand Down
3 changes: 2 additions & 1 deletion README_EN.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

## ✨ Features

- **🗂️ File Workbench**: file explorer (lazy-loading tree) + CodeMirror editor; inline preview for images / Markdown / HTML / PDF / Office
- **🗂️ File Workbench**: file explorer (lazy-loading tree, with pattern-based file/directory exclusion — the "Exclude patterns" setting, e.g. `*.meta`, `node_modules`; single `*` wildcard, case-insensitive) + CodeMirror editor; inline preview for images / Markdown / HTML / PDF / Office
- **🌐 Embedded Browser**: multiple web tabs with back / forward / refresh; content runs in a sandboxed iframe; external links open in the sidebar by default
- **💻 Real Terminal**: xterm.js + node-pty real shell, reconnect with transcript replay; optionally injects `terminal_*` tools for the model
- **🌿 Git Panel**: real diff + VSCode-style diff tabs, history, right-click to stage / commit / revert
Expand Down Expand Up @@ -251,6 +251,7 @@ pnpm watch # tsdown --watch
## ⚠️ Known Limitations

- Git has no push/pull/fetch; no file watcher (manual refresh); tool inline file-open buttons cannot be intercepted
- Explorer excludes are a display-layer filter: the truncation counter of very large directories (>1000 entries per level) may undercount after filtering
- Dragging a terminal tab to another pane remounts it (shell restarts)
- Office-suite preview (.docx/.xlsx/.pptx) moved to the recommended office plugin (see the "Add plugins" modals in settings); without it these files fall through to the code/download fallbacks
- Browser sandbox has no login state / third-party cookies are restricted; some sites need popup login; sites that refuse embedding via `X-Frame-Options`/`frame-ancestors` (e.g. arxiv.org) show a reason panel (with "Open in browser"); in-iframe navigation does not enter the back stack
Expand Down
122 changes: 122 additions & 0 deletions docs/plans/2026-08-14-explorer-exclude-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# 资源管理器:按模式排除文件/目录(issue #18)

**日期**:2026-08-14
**状态**:已实施(PR 待合)
**目标版本**:v0.11.0(待定)

## 1. 目标

让资源管理器可以像常见 IDE 一样按规则**隐藏**不需要的文件/目录(issue #18 的 Unity 场景:过滤 `.meta` 这类系统生成文件)。用户通过设置页为 explorer tab 维护一组「排除模式」,匹配的条目在树中不显示。

## 2. 非目标

- **不做 glob 全量支持**(`**`、`?`、`{a,b}`、`[x]` 等):无依赖优先,单 `*` 通配足够覆盖 `.meta` / `node_modules` / `build` 场景;需要更强模式时后续再加。
- **不默认过滤任何东西**:`explorerExclude` 默认 `[]`,行为与现状完全一致(符合项目「缺席 = 默认启用」惯例)。
- **不碰 host 半**:过滤是纯显示层偏好,host 的 `fs.tree` 路由、`listDirectory`、其他消费者(git/editor 等)全部不动。目录超大(>1000 项)时 `truncated` 计数可能把本可显示的行挤掉——可接受,文档已知限制记录。
- **不做 .gitignore 解析 / 内置噪声规则预设**:保持可预期,规则完全由用户显式声明。

## 3. 现状回顾

- `ExplorerView`(`src/client/ExplorerView.tsx`)懒加载目录树:`api.fsTree` → `fs.tree` 路由 → host `listDirectory` 返回 `SidebarFsEntry[]`(`name/path/isDir/hidden`),client 渲染时目录优先排序,dotfile 变暗显示。
- 设置体系:`SidebarPrefs`(`prefs-shared.ts` 共享类型 + `config.ts` schemastery schema + `client/prefs.ts` parsePrefs 校验 + `SidebarStore` 持有 + `/sidebar/settings` RPC 持久化)。设置页 `SideCardSection` 按注册表渲染 tab/viewer 卡片,卡片齿轮弹窗渲染 `settings.toggles`(目前仅布尔复选框行)。
- explorer tab 在 `builtins/tabs.tsx` 注册,`component` 收到 `{ ctx, store, scope, expanded, onToggleDir, onReferenceFile }`,`store` 是 `SidebarStore`(`getPrefs()` / `subscribe()` 可用)。

## 4. 设计

### 4.1 模式匹配(新 `src/exclude-patterns.ts`,纯函数、零依赖)

```ts
/** 单模式匹配:单个 '*' 通配 0+ 字符;无 '*' = 精确名称匹配;大小写不敏感。 */
export function matchesExcludePattern(name: string, pattern: string): boolean

/** 任一模式命中(空模式/空白串忽略)。 */
export function isExcludedName(name: string, patterns: readonly string[]): boolean
```

- 匹配对象是**条目名**(`entry.name`),不是完整路径——目录和文件一视同仁(`node_modules`、`build`、`*.meta` 都自然成立),且不需要从 client 向 host 传 cwd 相关逻辑。
- 大小写不敏感:跨平台一致(Windows/macOS 文件系统本身不敏感),用户心智中 `.meta` 与 `.META` 是同一文件。
- 实现:无 `*` 时直接 `toLowerCase()` 相等;有 `*` 时把模式转正则(其余字符 `RegExp.escape` 语义手写转义),`*` → `.*`,锚定 `^...$`。支持 `*.meta`、`build*`、`*.min.js`、`foo*bar`。
- 放 `src/` 根级共享(host/client 都能 import,当前只用 client;对齐 `prefs-shared.ts` 位置)。

### 4.2 偏好字段(三处同步)

`SidebarPrefs` 新增:

```ts
/** 资源管理器排除模式列表:匹配(条目名,单 '*' 通配,大小写不敏感)的条目在树中隐藏。 */
explorerExclude: string[]
```

- `prefs-shared.ts`:接口 + `SIDEBAR_PREFS_DEFAULTS.explorerExclude = []`
- `config.ts` `PrefsSchema`:`explorerExclude: z.array(z.string()).default([])`
- `client/prefs.ts` `parsePrefs`:新增 `stringArrayOf(record.explorerExclude)`——非数组回退 `[]`;数组过滤出非空 string、trim、去重、保序(对齐 `booleanMapOf` 的防御风格)。

### 4.3 设置 UI:声明式「文本行」

现有 `settings.toggles` 只支持布尔。扩展 `SidebarSettingsDeclaration` 增加**文本行**(多行文本框,每行一个模式):

```ts
/** 声明式文本设置:多行文本框(每行一个值),失焦/Enter 提交到 SidebarPrefs 数组字段。 */
export interface SidebarSettingText {
key: string // SidebarPrefs 数组字段名('explorerExclude')
title: string | (() => string)
desc?: string | (() => string)
placeholder?: string | (() => string)
}

export interface SidebarSettingsDeclaration {
toggles?: readonly SidebarSettingToggle[]
texts?: readonly SidebarSettingText[] // 新增
}
```

- 弹窗 `FeatureSettingsRows` 支持两种行:复选框行(现逻辑不变)+ 文本行(新组件 `TextSettingRow`:textarea + title/desc/placeholder)。
- 文本行交互:focus 时从 `prefs[key]` 初始化 draft;编辑为本地 state;blur(或 Enter,非 Shift)时 trim 行 → 去空 → 去重 → `commit({ [key]: lines })`;提交成功/失败后 `applyOutcome` 更新 prefs,draft 随 prefs 同步(`useEffect` 监听外部变化,避免并发写后残留)。
- 弹窗宽度/样式沿用现有 `popupRows`;textarea 样式新增(多行、等宽字体、行间紧凑)。

### 4.4 explorer tab 声明

`builtins/tabs.tsx` explorer 描述符加:

```ts
settings: {
texts: [{
key: 'explorerExclude',
title: () => t('settingsExplorerExcludeTitle'),
desc: () => t('settingsExplorerExcludeDesc'),
placeholder: () => t('settingsExplorerExcludePlaceholder'),
}],
}
```

卡片齿轮按钮的显隐条件 `(tab.settings?.toggles?.length ?? 0) > 0` 扩展为 `> 0 || texts > 0`。

### 4.5 ExplorerView 过滤

- `ExplorerView` 新增 prop `exclude: string[]`,由 `builtins/tabs.tsx` 传入 `store.getPrefs().explorerExclude`。
- `ExplorerView` 内部订阅 prefs 变更(新 prop `store` 传入,`useEffect(() => store.subscribe(...))` + useState 快照),排除规则变化即时生效。
- `renderLevel` 渲染前 `entries.filter(e => !isExcludedName(e.name, exclude))`。被过滤目录不渲染;`expanded` 中残留被过滤目录的路径时照常 loadDir(数据无害),仅不渲染。
- 现有能力不变:被过滤文件不显示,因此右键复制路径 / 打开 / @引用对它们不可达(符合「隐藏」语义)。

### 4.6 i18n

`locales.ts` zh/en 各加 3 条:

| key | zh | en |
|---|---|---|
| settingsExplorerExcludeTitle | 排除模式 | Exclude patterns |
| settingsExplorerExcludeDesc | 匹配(名称,* 通配,大小写不敏感)的条目在树中隐藏,每行一个 | Entries whose name matches (single `*` wildcard, case-insensitive) are hidden from the tree; one per line |
| settingsExplorerExcludePlaceholder | 例如:\*.meta、.DS_Store、node_modules | e.g. \*.meta, .DS_Store, node_modules |

## 5. 测试计划

1. `tests/exclude-patterns.spec.ts`:精确名、`*` 前缀/后缀/中间、大小写、空模式/空白串、正则特殊字符转义(`foo[bar]`、`a+b`)。
2. `tests/unit.spec.ts` 追加 `parsePrefs` 的 `explorerExclude` 用例:缺省/非法值回退 `[]`、过滤非 string/空串、trim、去重保序。
3. `tests/side-card-section.spec.tsx` 追加:声明 `texts` 的 tab 卡片有齿轮;`FeatureSettingsRows` 渲染文本行(title/desc/placeholder 出现、textarea 值 = 模式 join('\n'));`onCommitText` 提交解析后的行。
4. `tests/builtins.spec.ts` 追加:explorer 描述符声明 `settings.texts` 且 key 为 `explorerExclude`。
5. ExplorerView 过滤渲染测试(`tests/` 新增或并入现有):mock `api.fsTree` 返回含 `.meta` 的 entries,`exclude: ['*.meta']` 时渲染不含该行;空规则时全部渲染。

## 6. 文档

- `README.md` 功能一览「资源管理器」补一句:支持按模式隐藏文件/目录(设置页可配,如 `*.meta`);「已知限制」补:过滤是显示层,超大目录(>1000 项)的截断计数在过滤后可能偏少。
- `README_EN.md` 同步。
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,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"
}
Loading