feat(file-icon-theme): 可选彩色图标主题接线(懒加载 chunk + 设置开关 + i18n)

- 新 chunk `file-icons`(`lib/client-file-icons.js`,255 kB / gzip 90 kB):把数据表
  注册成 `registerFileIcon` 描述符——按 glyph+color 归并,563 条规则 → 约 250 条注册;
  目录规则只走 `folderNames`(只认领列出的目录,未列出的仍用内置 glyph)。
- `src/client/file-icon-theme.ts`:pref → loadChunk → 注册/注销,generation 守卫
  丢弃「切换中途落地」的加载;chunk 拉取失败只 log,保持内置 glyph。
- pref `fileIconTheme`(默认 `'builtin'`,未知值按 builtin 处理)+ 设置页
  「文件 → 文件图标」下拉(内置单色 / 彩色品牌图标)。
- chunk 名同步四处(chunk-loader / bundle-route / tsdown / manifest-consistency)+
  `bundle-route.spec`;tsdown 为该 chunk 加 react-icons ESM alias——否则整包打进
  6.4 MB,alias 后 255 kB。
- i18n:6 个新 key 镜像到全部 19 份词典(zh/en 之外),locale 键集守护通过。
- 皮肤契约:指南 §12 增豁免条款(品牌色是内容非 chrome,仅限该懒加载 chunk),
  AGENTS §5 同步;`tests/theme.spec.ts` 新增边界用例(核心图标模块零颜色字面量、
  核心不静态 import 该 chunk)。
- 测试:`tests/file-icon-theme.spec.tsx` 7 例(loader 六态 + 真实数据三表注册);
  prefs 新增默认值/解析用例;builtins / prefs / plugin-shape / smoke 夹具补字段。
- 全量 `pnpm test` 失败集与 main 基线逐条一致(5 文件 / 26 例预存环境失败),
  新增用例全绿;typecheck / lint / build / check:consumer-types 全通过。
This commit is contained in:
Menghuan1918
2026-09-08 21:43:28 +08:00
parent 8909455df2
commit 162c16ec32
41 changed files with 603 additions and 16 deletions
+1 -1
View File
@@ -59,7 +59,7 @@
- **构建纯度门**:client bundle 禁止 value-import `@dsh-external/*` 或非白名单 `@deepseek-ai/*`(`tsdown.config.ts` 拦截);`import type {}` 被擦除不触发——类型可共享,运行时符号不行;跨插件交互走 `ctx.betterSidebar` 方法调用。
- **懒加载 chunk**:重依赖(xterm/CodeMirror/mermaid)在独立 bundle(`lib/client-<name>.js`),经 `/sidebar/bundle` 按需下发、`globalThis.__dshChunks__` 物化(`src/client/chunk-loader.ts`),**核心 bundle 禁止静态 import `src/client/chunks/*`**。
- **i18n**:词典在 `betterSidebar` 命名空间,跟随 DSH `ctx.locale`;**新增 zh key 必须同步 `src/client/locales-ja.ts` 的 ja 翻译**(否则 ja 下回退 en)。渲染 `MarkdownText` 必须经 `markdownTextProps()`(§3 第 3 条)。
- **皮肤契约**:视觉值只消费 `--dsw-alias-*` / `--dsw-font-*` / `--ds-*` 令牌,无硬编码颜色;契约全文与 titleBar 四方案模型见[指南 §12](docs/external-plugin-guide.md),改动必须同步该节与 `tests/theme.spec.ts`。
- **皮肤契约**:视觉值只消费 `--dsw-alias-*` / `--dsw-font-*` / `--ds-*` 令牌,无硬编码颜色(**唯一豁免**:可选彩色图标主题 `src/client/chunks/file-icons.tsx`,品牌色是内容非 chrome,且只在懒加载 chunk + 用户显式开启;核心图标模块零颜色字面量由 `tests/theme.spec.ts` 守护);契约全文与 titleBar 四方案模型见[指南 §12](docs/external-plugin-guide.md),改动必须同步该节与 `tests/theme.spec.ts`。
- **契约反向引用**:皮肤契约被 `src/client/shell-presets.ts`、`tests/e2e/mount.e2e.ts` 的注释以「指南 §12」引用——调整指南章节结构时同步检查这两处。
- **接入 API 即文档**:`src/client/service.ts` 与 `src/client/builtins/` 的任何行为变更,必须同步 [docs/external-plugin-guide.md](docs/external-plugin-guide.md)(唯一权威接入文档,不再双份维护)。
+2 -1
View File
@@ -265,7 +265,8 @@ GitHub topic [`dsh-better-sidebar`](https://github.com/topics/dsh-better-sidebar
### v0.19.0(未发布)
- 🎨 **文件图标多样化 + 对外图标注册 API([file-icons.tsx](./src/client/file-icons.tsx))**:文件树与编辑器文件 tab 不再是单一 `VscFile`——markdown / 图片媒体 / PDF / JSON / 40+ 代码扩展 / 配置 / 数据库 / lock / 压缩包各有专属 glyph(VSCodicons 单色 `currentColor`,遵循皮肤契约),未知扩展回退通用图标。`ctx.betterSidebar` 新增 `registerFileIcon`(`features` 含 `'fileIcons'`):按扩展名注册自定义图标(彩色 ReactNode 亦可,颜色责任在注册方),`exts: []` 为全局默认(只兜内置 glyph 没认领的扩展,不吞掉内置多样性),保留扩展名 `'folder'` / `'folder-open'` 可换目录行图标;`fileIcon` / `folderIcon` 为权威解析器(完整回退链 + 逐工厂崩溃隔离),注册/注销即时生效。接入示例见[外部插件指南 §7](./docs/external-plugin-guide.md)。
- 🎨 **可选彩色图标主题**:设置页「文件 → 文件图标」可在**内置单色**与**彩色品牌图标**之间切换——彩色主题含 563 条规则(218 扩展名 + 197 精确文件名 + 148 目录名,如 `.tsx → React`、`package.json → npm`、`node_modules` 着色),数据在**懒加载 chunk**(`lib/client-file-icons.js`)里,只有选中时才下载,默认关闭时启动零开销。数据源自 [#429](https://github.com/omdsh-dev/DSH-better-sidebar/pull/429)(@fenter)。
- 🎨 **文件图标多样化 + 对外图标注册 API([file-icons.tsx](./src/client/file-icons.tsx))**:文件树与编辑器文件 tab 不再是单一 `VscFile`——markdown / 图片媒体 / PDF / JSON / 40+ 代码扩展 / 配置 / 数据库 / lock / 压缩包各有专属 glyph(VSCodicons 单色 `currentColor`,遵循皮肤契约),未知扩展回退通用图标。`ctx.betterSidebar` 新增 `registerFileIcon`(`features` 含 `'fileIcons'`):按扩展名 / 精确文件名(`names`)/ 目录名(`folderNames`)注册自定义图标(彩色 ReactNode 亦可,颜色责任在注册方),`exts: []` 为全局默认(只兜内置 glyph 没认领的扩展,不吞掉内置多样性),保留扩展名 `'folder'` / `'folder-open'` 可换目录行图标;`fileIcon` / `folderIcon` 为权威解析器(完整回退链 + 逐工厂崩溃隔离),注册/注销即时生效。接入示例见[外部插件指南 §7](./docs/external-plugin-guide.md)。
### v0.18.1
+17
View File
@@ -722,6 +722,16 @@ if (ctx.betterSidebar.features.includes('fileIcons')) {
注册/注销即时生效(文件树与 tab 栏订阅注册表变化自动重渲染);图标工厂抛错会被吞掉
(console.error 后跳到回退链下一级),不会空白行。
**内置的可选彩色图标主题**(v0.19.0+,设置页「文件 → 文件图标」):
- `'builtin'`(默认)=上面的单色 glyph 映射,零额外加载。
- `'colored'`=563 条品牌/通用彩色规则(218 扩展名 + 197 文件名 + 148 目录名),
数据在**懒加载 chunk** `lib/client-file-icons.js`(`/sidebar/bundle/file-icons.js`),
只有用户选中时才拉取,选中后走 `registerFileIcon` 注册、切回即注销(`src/client/file-icon-theme.ts`)。
数据来自 [PR #429](https://github.com/omdsh-dev/DSH-better-sidebar/pull/429)(@fenter)。
- 因此彩色图标是**皮肤契约 §12 的唯一豁免面**:品牌色是内容而非 chrome,不能走
`--dsw-alias-*`;豁免只覆盖这个 chunk(核心图标模块零颜色字面量,由 `tests/theme.spec.ts` 守护)。
**版本与能力探测**(v0.12.0+):消费插件先查能力再使用新 API,老版本(或旧 DSH)下优雅降级:
```ts
@@ -852,6 +862,13 @@ ctx.effect(() =>
## 12. 皮肤兼容(令牌驱动)
> better-sidebar 所有视觉值消费 DSH 的 `--dsw-alias-*` / `--dsw-font-*` / `--ds-*` 令牌(无硬编码颜色),**不做每皮肤适配**。已与 dsh-web-ui 皮肤中心兼容(10 款皮肤全覆盖 `--dsw-alias-*` 层;`tests/theme.spec.ts` 守护)。你的 tab/viewer 组件遵循同样的令牌规则即可自动兼容全部皮肤。
>
> **唯一豁免面**:可选彩色文件图标主题(`fileIconTheme: 'colored'`)在
> `src/client/chunks/file-icons.tsx` 内硬编码品牌色——品牌色是内容标识而非 chrome,无法
> 映射到语义令牌;豁免的三个前提是「用户显式开启」「数据只在懒加载 chunk」「核心图标
> 模块零颜色字面量」(`tests/theme.spec.ts` 的 boundary 用例守护)。插件自己注册的
> 图标(`registerFileIcon`)颜色由注册方负责,同样不受本节令牌约束,但**不要**把彩色
> 图标塞进核心 bundle 的常驻渲染路径。
### 12.1 规则
+1 -1
View File
@@ -19,7 +19,7 @@ import { fileURLToPath } from 'node:url'
import type { Context, SidebarHttpRequest, SidebarHttpResponse } from './context-types.ts'
/** The chunk names the client may request (mirror of src/client/chunk-loader.ts). */
export const CHUNK_NAMES = ['terminal', 'editor', 'mermaid', 'locale'] as const
export const CHUNK_NAMES = ['terminal', 'editor', 'mermaid', 'locale', 'file-icons'] as const
export type ChunkName = (typeof CHUNK_NAMES)[number]
/** Directory of this host-half module (lib/ — the chunk scripts live next to it). */
+23 -1
View File
@@ -8,7 +8,7 @@
* `browser:<n>` the same way (no quota). The editor IS the files window
* (the old standalone explorer merged into it).
*/
import { IconCodeOutline16, IconFolderOpen16, IconNewChatOutline16, IconPanelLeftOutline16, IconThinkOutline16 } from '@deepseek-ai/dsh-client-ui-primitives'
import { IconCodeOutline16, IconFolderOpen16, IconFolderOpenOutline16, IconNewChatOutline16, IconPanelLeftOutline16, IconSparkle16, IconThinkOutline16 } from '@deepseek-ai/dsh-client-ui-primitives'
import type { Context } from '../../context-types.ts'
import { allLeaves, isAgentTabId, type SidebarState } from '../state.ts'
import { t } from '../locales.ts'
@@ -121,6 +121,28 @@ export function builtinTabs(ctx: Context, options: BuiltinTabOptions = {}): read
key: 'workspaceFence',
title: () => t('settingsFenceTitle'),
desc: () => t('settingsFenceDesc'),
}, {
// The file-icon theme: monochrome built-in glyphs (default) vs the
// optional colored brand dataset, which is fetched from the lazy
// `file-icons` chunk only when picked (see file-icon-theme.ts).
key: 'fileIconTheme',
type: 'select',
title: () => t('settingsFileIconThemeTitle'),
desc: () => t('settingsFileIconThemeDesc'),
options: [
{
value: 'builtin',
icon: (size: number) => <IconFolderOpenOutline16 size={size} />,
title: () => t('settingsFileIconThemeBuiltin'),
desc: () => t('settingsFileIconThemeBuiltinDesc'),
},
{
value: 'colored',
icon: (size: number) => <IconSparkle16 size={size} />,
title: () => t('settingsFileIconThemeColored'),
desc: () => t('settingsFileIconThemeColoredDesc'),
},
],
}],
render: ({ pluginSettings, updatePluginSetting }) => (
<OpenWithSettings pluginSettings={pluginSettings} updatePluginSetting={updatePluginSetting} />
+1 -1
View File
@@ -50,7 +50,7 @@
* client.js); an edit that does land while a core HMR happens is caught by
* the ETag comparison on the next activation.
*/
export type ChunkName = 'terminal' | 'editor' | 'mermaid' | 'locale'
export type ChunkName = 'terminal' | 'editor' | 'mermaid' | 'locale' | 'file-icons'
/** The module exports a chunk factory provides (namespace-ish record). */
export type ChunkExports = Record<string, unknown>
+80 -1
View File
@@ -20,6 +20,7 @@
* imported by the core bundle; keep it that way.
*/
import type { ReactNode } from 'react'
import type { BetterSidebarService, FileIconDescriptor } from '../service.ts'
import {
SiAngular, SiApple, SiAstro, SiBabel, SiBiome, SiBun, SiC, SiClojure,
SiCmake, SiCoffeescript, SiComposer, SiConventionalcommits, SiCplusplus,
@@ -38,7 +39,7 @@ import {
} from 'react-icons/si'
import {
VscBook, VscFile, VscFileBinary, VscFileCode, VscFileMedia, VscFilePdf,
VscFileText, VscFileZip, VscJson, VscKey,
VscFileText, VscFileZip, VscFolder, VscFolderOpened, VscJson, VscKey,
VscMap, VscMusic, VscSettings, VscTerminalCmd, VscTerminalPowershell
} from 'react-icons/vsc'
import type { IconType } from 'react-icons'
@@ -692,3 +693,81 @@ export const FOLDER_COLOR: Record<string, string> = {
ios: '#000000',
Platforms: '#3178C6',
}
// ── Registration ────────────────────────────────────────────────────────
/**
* Register the dataset through the public `registerFileIcon` API: one
* descriptor per distinct glyph+color (the resolver walks descriptors per
* row, so 563 rules collapse into ~250 registrations), plus one per folder
* tint. Returns a disposer that removes every registration.
*
* Folder tints ride `folderNames` only — they claim the directories they
* name, never every folder, so unnamed directories keep the built-in glyphs.
*/
export function registerColoredFileIcons(service: BetterSidebarService): () => void {
const disposers: (() => void)[] = []
const register = (descriptor: FileIconDescriptor): void => {
disposers.push(service.registerFileIcon(descriptor))
}
let seq = 0
// Extensions: group the table by (glyph, color) → one descriptor per group.
for (const [Icon, byColor] of groupByGlyph(FILE_ICON_BY_EXT)) {
for (const [color, exts] of byColor) {
register({
id: `colored:ext:${seq++}`,
exts,
icon: (_path, size) => colored({ Icon, color }, size),
})
}
}
// Exact file names (package.json, Dockerfile, …): same grouping, name rules.
for (const [Icon, byColor] of groupByGlyph(FILE_ICON_BY_NAME)) {
for (const [color, names] of byColor) {
register({
id: `colored:name:${seq++}`,
names,
icon: (_path, size) => colored({ Icon, color }, size),
})
}
}
// Directory tints: VscFolder/VscFolderOpened in the rule's color, grouped
// by color (the folder table maps name → color, not name → glyph).
const foldersByColor = new Map<string, string[]>()
for (const [name, color] of Object.entries(FOLDER_COLOR)) {
const names = foldersByColor.get(color)
if (names === undefined) foldersByColor.set(color, [name])
else names.push(name)
}
for (const [color, folderNames] of foldersByColor) {
register({
id: `colored:folder:${seq++}`,
folderNames,
icon: (_path, size, open) => open === true
? <VscFolderOpened size={size} style={{ color }} />
: <VscFolder size={size} style={{ color }} />,
})
}
return () => { for (const dispose of disposers) dispose() }
}
/** Regroup one name→{Icon,color} table into Icon → color → keys. */
function groupByGlyph(table: Partial<Record<string, IconEntry>>): Map<IconType, Map<string, string[]>> {
const grouped = new Map<IconType, Map<string, string[]>>()
for (const [key, entry] of Object.entries(table)) {
if (entry === undefined) continue
let byColor = grouped.get(entry.Icon)
if (byColor === undefined) {
byColor = new Map()
grouped.set(entry.Icon, byColor)
}
const keys = byColor.get(entry.color)
if (keys === undefined) byColor.set(entry.color, [key])
else keys.push(key)
}
return grouped
}
+80
View File
@@ -0,0 +1,80 @@
/**
* The optional colored file-icon theme (pref `fileIconTheme`).
*
* `'builtin'` (the default) is the monochrome glyph map the tree already
* uses — nothing to do. `'colored'` fetches the lazy `file-icons` chunk
* (~160 brand glyphs + 563 rules) and registers it through the public
* `registerFileIcon` API; switching back disposes those registrations. The
* chunk is only fetched when the user picks the theme, so the startup bundle
* never carries icon data it may not render.
*
* The loader is generation-guarded: a fast toggle (or an unload) while the
* chunk is still in flight must not register the stale dataset.
*/
import { loadChunk } from './chunk-loader.ts'
import type { BetterSidebarService } from './service.ts'
/** The pref value selecting the built-in monochrome glyphs (default). */
export const FILE_ICON_THEME_BUILTIN = 'builtin'
/** The pref value selecting the optional colored brand-icon dataset. */
export const FILE_ICON_THEME_COLORED = 'colored'
/** A registrar exported by the lazy `file-icons` chunk. */
type ColoredRegistrar = (service: BetterSidebarService) => () => void
export interface FileIconThemeLoader {
/** Apply a pref value (unknown values behave as the built-in theme). */
set(theme: string): void
/** Drop any registration and stop reacting to in-flight chunk loads. */
dispose(): void
}
/**
* Bind `service` to the `fileIconTheme` pref. Call `set` on activation and on
* every prefs change; call `dispose` when the plugin unloads.
*/
export function createFileIconThemeLoader(service: BetterSidebarService): FileIconThemeLoader {
/** Disposer of the currently registered colored dataset, if any. */
let unregister: (() => void) | undefined
/** Bumped on every set/dispose: a load that lands on a stale generation is dropped. */
let generation = 0
/** The theme the pref currently asks for (repeated set() calls are no-ops). */
let current: string | undefined
const dropRegistrations = (): void => {
const dispose = unregister
unregister = undefined
if (dispose !== undefined) dispose()
}
const set = (theme: string): void => {
if (theme === current) return
current = theme
const generationAtSet = ++generation
if (theme !== FILE_ICON_THEME_COLORED) {
dropRegistrations()
return
}
loadChunk('file-icons').then((chunk) => {
if (generationAtSet !== generation) return
const register = chunk.registerColoredFileIcons as ColoredRegistrar | undefined
if (typeof register !== 'function') {
console.error('[dsh-better-sidebar] file-icons chunk exports no registerColoredFileIcons')
return
}
unregister = register(service)
}).catch((error: unknown) => {
if (generationAtSet !== generation) return
console.error('[dsh-better-sidebar] file-icons chunk failed to load:', error)
})
}
return {
set,
dispose: () => {
generation += 1
current = undefined
dropRegistrations()
},
}
}
+16
View File
@@ -20,6 +20,7 @@ import { RenderBoundary } from './RenderBoundary.tsx'
import { registerOpenPathInterception, registerTurnTailInterception } from './intercept.tsx'
import { registerLinkInterception } from './link-intercept.ts'
import { registerImeGuard } from './ime-guard.ts'
import { createFileIconThemeLoader } from './file-icon-theme.ts'
import { registerSettingsNavIcon } from './settings-nav-icon.ts'
import { loadBootDecision } from './prefs.ts'
import { SideCardSection } from './SideCardSection.tsx'
@@ -164,6 +165,21 @@ export function apply(ctx: Context): void {
() => registerBuiltins(ctx, service, { terminalTitle: () => terminalTitle }),
'dsh-better-sidebar: register built-in tabs and viewers',
)
// Optional colored file-icon theme: the `fileIconTheme` pref decides whether
// the lazy `file-icons` chunk is fetched and its dataset registered through
// `registerFileIcon`. The loader reacts to prefs writes and is disposed with
// the fiber (HMR-safe); 'builtin' (the default) keeps the startup path free
// of icon data.
ctx.effect(() => {
const loader = createFileIconThemeLoader(service)
const sync = (): void => { loader.set(sidebarStore.getPrefs().fileIconTheme) }
sync()
const unsubscribe = service.subscribeState(sync)
return () => {
unsubscribe()
loader.dispose()
}
}, 'dsh-better-sidebar: colored file-icon theme')
// A failure anywhere in the client lifecycle must never take the app down
// silently: log with the plugin prefix and pin a visible diagnostic strip
// to the page so a blank panel is never the only symptom. This strip is
+6
View File
@@ -267,6 +267,12 @@ export const ar: Record<string, string> = {
settingsToolsDesc: 'عند التفعيل، يمكن للنموذج إنشاء وتشغيل طرفيات الشريط الجانبي عبر أدوات terminal_* الثمانية (معطّل افتراضياً)',
settingsFenceTitle: 'فحص مسارات مساحة العمل',
settingsFenceDesc: 'عند التفعيل، تصل وظائف الملفات في الشريط الجانبي إلى مسارات داخل مساحة عمل الجلسة فقط (الافتراضي)؛ عند الإيقاف تصبح ملفات المضيف كلها متاحة — وتحصل سكربتات الصفحة على النفاذ نفسه طوال ذلك',
settingsFileIconThemeTitle: 'أيقونات الملفات',
settingsFileIconThemeDesc: 'نمط الأيقونات لشجرة الملفات وتبويبات الملفات (تُحمَّل السمة الملوّنة عند الطلب، لا عند بدء التشغيل)',
settingsFileIconThemeBuiltin: 'مدمج (أحادي اللون)',
settingsFileIconThemeBuiltinDesc: 'أيقونات أحادية اللون تتبع رموز السمة، تغطي أنواع التعليمات البرمجية والمستندات والوسائط الشائعة',
settingsFileIconThemeColored: 'أيقونات العلامات الملوّنة',
settingsFileIconThemeColoredDesc: 'ألوان العلامات حسب النوع (مثل .tsx → React و package.json → npm)؛ تُحمَّل حزمة الأيقونات عند أول تفعيل',
fenceErrorReason: 'هذا المسار خارج مساحة عمل الجلسة وقد حجبه الفحص',
fenceDisableAction: 'إيقاف فحص مساحة العمل',
settingsBottomTerminalTitle: 'فتح طرفية تلقائياً عند أول توسيع للوحة السفلية',
+6
View File
@@ -252,6 +252,12 @@ export const de: Record<string, string> = {
settingsToolsDesc: 'Wenn aktiviert, kann das Modell über die 8 terminal_*-Werkzeuge Terminale in der Seitenleiste erstellen und steuern (standardmäßig deaktiviert)',
settingsFenceTitle: 'Workspace-Pfadkontrolle',
settingsFenceDesc: 'Wenn aktiv, erreichen die Dateifunktionen der Seitenleiste nur Pfade innerhalb des Session-Workspaces (Standard); ausgeschaltet sind Dateien überall auf dem Host erreichbar — Skripte der Seite erhalten solange dieselbe Reichweite',
settingsFileIconThemeTitle: 'Dateisymbole',
settingsFileIconThemeDesc: 'Symbolstil für Dateibaum und Datei-Tabs (das Farbthema lädt bei Bedarf, nie beim Start)',
settingsFileIconThemeBuiltin: 'Integriert (einfarbig)',
settingsFileIconThemeBuiltinDesc: 'Token-gesteuerte einfarbige Symbole für gängige Code-, Dokument- und Medientypen',
settingsFileIconThemeColored: 'Farbige Markensymbole',
settingsFileIconThemeColoredDesc: 'Markenfarben je Typ (z. B. .tsx → React, package.json → npm); das Icon-Paket lädt beim ersten Aktivieren',
fenceErrorReason: 'Dieser Pfad liegt außerhalb des Session-Workspaces und wurde von der Workspace-Kontrolle blockiert',
fenceDisableAction: 'Workspace-Kontrolle ausschalten',
settingsBottomTerminalTitle: 'Beim ersten Ausklappen des unteren Panels automatisch ein Terminal öffnen',
+6
View File
@@ -259,6 +259,12 @@ export const fr: Record<string, string> = {
settingsToolsDesc: 'Une fois activé, le modèle peut créer et piloter des terminaux de la barre latérale via les 8 outils terminal_* (désactivé par défaut)',
settingsFenceTitle: 'Contrôle des chemins de l\'espace de travail',
settingsFenceDesc: 'Activé, les fonctions fichiers de la barre latérale n\'atteignent que les chemins dans l\'espace de travail de la session (par défaut) ; désactivé, les fichiers de tout l\'hôte deviennent accessibles — les scripts de la page obtiennent la même portée pendant ce temps',
settingsFileIconThemeTitle: 'Icônes de fichiers',
settingsFileIconThemeDesc: 'Style des icônes de l\'arborescence et des onglets de fichiers (le thème coloré se charge à la demande, jamais au démarrage)',
settingsFileIconThemeBuiltin: 'Intégré (monochrome)',
settingsFileIconThemeBuiltinDesc: 'Icônes monochromes pilotées par les jetons de thème, pour les types code / document / média courants',
settingsFileIconThemeColored: 'Icônes de marque colorées',
settingsFileIconThemeColoredDesc: 'Couleurs de marque par type (ex. .tsx → React, package.json → npm) ; le pack d\'icônes se charge à la première activation',
fenceErrorReason: 'Ce chemin est hors de l\'espace de travail de la session et a été bloqué par le contrôle',
fenceDisableAction: 'Désactiver le contrôle de l\'espace de travail',
settingsBottomTerminalTitle: 'Ouvrir automatiquement un terminal au premier déploiement du panneau inférieur',
+6
View File
@@ -266,6 +266,12 @@ export const hi: Record<string, string> = {
settingsToolsDesc: 'सक्षम होने पर, मॉडल 8 terminal_* टूल्स से साइडबार टर्मिनल बना और चला सकता है (डिफ़ॉल्ट रूप से बंद)',
settingsFenceTitle: 'वर्कस्पेस पाथ जाँच',
settingsFenceDesc: 'चालू होने पर साइडबार की फ़ाइल सुविधाएँ केवल सेशन वर्कस्पेस के भीतर के पाथ तक पहुँचती हैं (डिफ़ॉल्ट); बंद करने पर होस्ट की कोई भी फ़ाइल खोली जा सकती है — उस दौरान पेज के स्क्रिप्ट को भी वही पहुँच मिल जाती है',
settingsFileIconThemeTitle: 'फ़ाइल आइकन',
settingsFileIconThemeDesc: 'फ़ाइल ट्री और फ़ाइल टैब के आइकन की शैली (रंगीन थीम माँग पर लोड होती है, शुरू होते समय नहीं)',
settingsFileIconThemeBuiltin: 'अंतर्निहित (एकरंगी)',
settingsFileIconThemeBuiltinDesc: 'स्किन टोकन के अनुसार एकरंगी आइकन, सामान्य कोड / दस्तावेज़ / मीडिया प्रकारों के लिए',
settingsFileIconThemeColored: 'रंगीन ब्रांड आइकन',
settingsFileIconThemeColoredDesc: 'प्रकार के अनुसार ब्रांड रंग (जैसे .tsx → React, package.json → npm); पहली बार चालू करने पर आइकन पैक लोड होता है',
fenceErrorReason: 'यह पाथ सेशन वर्कस्पेस के बाहर है और जाँच द्वारा अवरुद्ध किया गया',
fenceDisableAction: 'वर्कस्पेस जाँच बंद करें',
settingsBottomTerminalTitle: 'निचला पैनल पहली बार खुलने पर स्वतः टर्मिनल खोलें',
+6
View File
@@ -264,6 +264,12 @@ export const id: Record<string, string> = {
settingsToolsDesc: 'Saat diaktifkan, model dapat membuat dan mengendalikan terminal sidebar melalui 8 alat terminal_* (nonaktif secara default)',
settingsFenceTitle: 'Pemeriksaan path workspace',
settingsFenceDesc: 'Saat aktif, fitur berkas sidebar hanya menjangkau path di dalam workspace sesi (default); saat nonaktif, berkas di mana pun di host dapat diakses — skrip halaman memperoleh jangkauan yang sama selama itu',
settingsFileIconThemeTitle: 'Ikon berkas',
settingsFileIconThemeDesc: 'Gaya ikon untuk pohon berkas dan tab berkas (tema berwarna dimuat saat diperlukan, tidak saat mulai)',
settingsFileIconThemeBuiltin: 'Bawaan (monokrom)',
settingsFileIconThemeBuiltinDesc: 'Ikon monokrom mengikuti token tema, mencakup tipe kode / dokumen / media umum',
settingsFileIconThemeColored: 'Ikon merek berwarna',
settingsFileIconThemeColoredDesc: 'Warna merek per tipe (mis. .tsx → React, package.json → npm); paket ikon dimuat saat pertama diaktifkan',
fenceErrorReason: 'Path ini berada di luar workspace sesi dan diblokir oleh pemeriksaan',
fenceDisableAction: 'Matikan pemeriksaan workspace',
settingsBottomTerminalTitle: 'Buka terminal otomatis saat panel bawah pertama kali dibentangkan',
+6
View File
@@ -257,6 +257,12 @@ export const it: Record<string, string> = {
settingsToolsDesc: 'Se attivato, il modello può creare e gestire terminali della barra laterale attraverso gli 8 strumenti terminal_* (disattivato per impostazione predefinita)',
settingsFenceTitle: 'Controllo dei percorsi del workspace',
settingsFenceDesc: 'Attivo, le funzioni file della barra laterale raggiungono solo i percorsi nel workspace di sessione (predefinito); disattivo, ogni file dell\'host è raggiungibile — gli script della pagina ottengono la stessa portata nel frattempo',
settingsFileIconThemeTitle: 'Icone dei file',
settingsFileIconThemeDesc: 'Stile delle icone per l\'albero dei file e le schede (il tema colorato si carica su richiesta, mai all\'avvio)',
settingsFileIconThemeBuiltin: 'Integrato (monocromatico)',
settingsFileIconThemeBuiltinDesc: 'Icone monocromatiche basate sui token del tema, per i tipi di codice / documento / media più comuni',
settingsFileIconThemeColored: 'Icone di marca colorate',
settingsFileIconThemeColoredDesc: 'Colori di marca per tipo (es. .tsx → React, package.json → npm); il pacchetto si carica alla prima attivazione',
fenceErrorReason: 'Questo percorso è fuori dal workspace di sessione ed è stato bloccato dal controllo',
fenceDisableAction: 'Disattiva il controllo del workspace',
settingsBottomTerminalTitle: 'Apri automaticamente un terminale alla prima espansione del pannello inferiore',
+6
View File
@@ -266,6 +266,12 @@ export const ja: Record<string, string> = {
settingsToolsDesc: 'オンにすると、モデルが terminal_create 等 8 個のツールでサイドバーターミナルを作成・操作可能(デフォルトオフ)',
settingsFenceTitle: 'ワークスペースパスガード',
settingsFenceDesc: 'オンの場合、サイドバーのファイル機能はセッションワークスペース内のパスのみアクセス可能(デフォルト)。オフにするとホスト上の任意のファイルにアクセスできるが、オフの間はページ内スクリプトも同じ権限を得る',
settingsFileIconThemeTitle: 'ファイルアイコン',
settingsFileIconThemeDesc: 'ファイルツリーとファイルタブのアイコンスタイル(カラーテーマは必要時に読み込み、起動時には読み込みません)',
settingsFileIconThemeBuiltin: '標準(モノクロ)',
settingsFileIconThemeBuiltinDesc: 'スキントークンに従うモノクロアイコン。一般的なコード / ドキュメント / メディア形式に対応',
settingsFileIconThemeColored: 'カラーブランドアイコン',
settingsFileIconThemeColoredDesc: '種類ごとにブランドカラーで表示(例:.tsx → React、package.json → npm)。初回有効時にアイコンパックを読み込みます',
fenceErrorReason: 'このパスはセッションワークスペースの外にあり、ワークスペースガードによりブロックされました',
fenceDisableAction: 'ワークスペースガードをオフにする',
settingsBottomTerminalTitle: '下パネル初回展開時に自動でターミナルを開く',
+6
View File
@@ -258,6 +258,12 @@ export const ko: Record<string, string> = {
settingsToolsDesc: '켜면 모델이 terminal_create 등 8개 도구로 사이드바 터미널을 만들고 조작할 수 있습니다(기본 꺼짐)',
settingsFenceTitle: '워크스페이스 경로 검사',
settingsFenceDesc: '켜면 사이드바의 파일 기능이 세션 워크스페이스 내 경로만 접근합니다(기본값). 끄면 호스트의 모든 파일에 접근할 수 있으며, 꺼져 있는 동안 페이지 내 스크립트도 동일한 권한을 얻습니다',
settingsFileIconThemeTitle: '파일 아이콘',
settingsFileIconThemeDesc: '파일 트리와 파일 탭의 아이콘 스타일 (컬러 테마는 필요할 때 로드되며 시작 시에는 로드되지 않습니다)',
settingsFileIconThemeBuiltin: '기본(단색)',
settingsFileIconThemeBuiltinDesc: '스킨 토큰을 따르는 단색 아이콘으로 주요 코드 / 문서 / 미디어 형식을 지원합니다',
settingsFileIconThemeColored: '컬러 브랜드 아이콘',
settingsFileIconThemeColoredDesc: '유형별 브랜드 색상 (예: .tsx → React, package.json → npm), 처음 켤 때 아이콘 팩을 로드합니다',
fenceErrorReason: '이 경로는 세션 워크스페이스 밖에 있어 검사에 의해 차단되었습니다',
fenceDisableAction: '워크스페이스 검사 끄기',
settingsBottomTerminalTitle: '하단 패널을 처음 펼칠 때 터미널 자동 열기',
+6
View File
@@ -264,6 +264,12 @@ export const nl: Record<string, string> = {
settingsToolsDesc: 'Indien ingeschakeld kan het model zijbalk-terminals aanmaken en besturen via de 8 terminal_*-tools (standaard uit)',
settingsFenceTitle: 'Workspace-padcontrole',
settingsFenceDesc: 'Aan: de bestandsfuncties van de zijbalk bereiken alleen paden binnen de sessie-workspace (standaard); uit: elk bestand op de host is bereikbaar — paginascripts krijgen dat bereik zolang het uit staat',
settingsFileIconThemeTitle: 'Bestandspictogrammen',
settingsFileIconThemeDesc: 'Pictogramstijl voor de bestandsboom en bestandstabbladen (het kleurenthema wordt op verzoek geladen, nooit bij het starten)',
settingsFileIconThemeBuiltin: 'Ingebouwd (monochroom)',
settingsFileIconThemeBuiltinDesc: 'Monochrome pictogrammen volgens de token-regels, voor gangbare code-, document- en mediatypen',
settingsFileIconThemeColored: 'Gekleurde merkpictogrammen',
settingsFileIconThemeColoredDesc: 'Merkkleuren per type (bijv. .tsx → React, package.json → npm); het pictogrammenpakket wordt geladen bij de eerste activering',
fenceErrorReason: 'Dit pad ligt buiten de sessie-workspace en is geblokkeerd door de controle',
fenceDisableAction: 'Workspace-controle uitzetten',
settingsBottomTerminalTitle: 'Automatisch een terminal openen bij de eerste uitklap van het onderpaneel',
+6
View File
@@ -268,6 +268,12 @@ export const pl: Record<string, string> = {
settingsToolsDesc: 'Po włączeniu model może tworzyć i sterować terminalami panelu bocznego przez 8 narzędzi terminal_* (domyślnie wyłączone)',
settingsFenceTitle: 'Kontrola ścieżek obszaru roboczego',
settingsFenceDesc: 'Włączona: funkcje plików paska bocznego sięgają tylko ścieżek w obszarze roboczym sesji (domyślnie); wyłączona: dostępny jest dowolny plik hosta — skrypty strony zyskują ten sam zasięg przez ten czas',
settingsFileIconThemeTitle: 'Ikony plików',
settingsFileIconThemeDesc: 'Styl ikon drzewa plików i kart plików (kolorowy motyw wczytuje się na żądanie, nigdy przy starcie)',
settingsFileIconThemeBuiltin: 'Wbudowane (monochromatyczne)',
settingsFileIconThemeBuiltinDesc: 'Monochromatyczne ikony zgodne z tokenami motywu, dla typowych plików kodu / dokumentów / multimediów',
settingsFileIconThemeColored: 'Kolorowe ikony marek',
settingsFileIconThemeColoredDesc: 'Kolory marek według typu (np. .tsx → React, package.json → npm); paczka ikon wczytuje się przy pierwszym włączeniu',
fenceErrorReason: 'Ta ścieżka jest poza obszarem roboczym sesji i została zablokowana przez kontrolę',
fenceDisableAction: 'Wyłącz kontrolę obszaru roboczego',
settingsBottomTerminalTitle: 'Automatycznie otwieraj terminal przy pierwszym rozwinięciu panelu dolnego',
+6
View File
@@ -249,6 +249,12 @@ export const pt: Record<string, string> = {
settingsToolsDesc: 'Quando ativado, o modelo pode criar e operar terminais da barra lateral por meio das 8 ferramentas terminal_* (desativado por padrão)',
settingsFenceTitle: 'Controle de caminhos do workspace',
settingsFenceDesc: 'Ativado, os recursos de arquivos da barra lateral só alcançam caminhos dentro do workspace da sessão (padrão); desativado, arquivos de todo o host ficam acessíveis — scripts da página ganham o mesmo alcance enquanto isso',
settingsFileIconThemeTitle: 'Ícones de arquivo',
settingsFileIconThemeDesc: 'Estilo dos ícones da árvore de arquivos e das abas (o tema colorido é carregado sob demanda, nunca na inicialização)',
settingsFileIconThemeBuiltin: 'Integrado (monocromático)',
settingsFileIconThemeBuiltinDesc: 'Ícones monocromáticos guiados por tokens, para tipos comuns de código / documento / mídia',
settingsFileIconThemeColored: 'Ícones de marca coloridos',
settingsFileIconThemeColoredDesc: 'Cores de marca por tipo (ex.: .tsx → React, package.json → npm); o pacote é carregado ao ativar pela primeira vez',
fenceErrorReason: 'Este caminho está fora do workspace da sessão e foi bloqueado pelo controle',
fenceDisableAction: 'Desativar o controle do workspace',
settingsBottomTerminalTitle: 'Abrir um terminal automaticamente na primeira expansão do painel inferior',
+6
View File
@@ -263,6 +263,12 @@ export const ru: Record<string, string> = {
settingsToolsDesc: 'Если включено, модель может через 8 инструментов (terminal_create и др.) создавать терминалы боковой панели и управлять ими (по умолчанию выключено)',
settingsFenceTitle: 'Контроль путей рабочей области',
settingsFenceDesc: 'Включено — файловые функции боковой панели достигают только путей внутри рабочей области сессии (по умолчанию); выключено — доступны любые файлы хоста, а скрипты страницы получают тот же доступ всё это время',
settingsFileIconThemeTitle: 'Значки файлов',
settingsFileIconThemeDesc: 'Стиль значков дерева файлов и вкладок (цветная тема загружается по требованию, а не при запуске)',
settingsFileIconThemeBuiltin: 'Встроенные (монохромные)',
settingsFileIconThemeBuiltinDesc: 'Монохромные значки на основе токенов темы: код, документы и медиа',
settingsFileIconThemeColored: 'Цветные значки брендов',
settingsFileIconThemeColoredDesc: 'Цвета брендов по типу (например, .tsx → React, package.json → npm); набор значков загружается при первом включении',
fenceErrorReason: 'Этот путь вне рабочей области сессии и заблокирован контролем',
fenceDisableAction: 'Отключить контроль рабочей области',
settingsBottomTerminalTitle: 'Автооткрытие терминала при первом разворачивании нижней панели',
+6
View File
@@ -249,6 +249,12 @@ export const sv: Record<string, string> = {
settingsToolsDesc: 'När aktiverat kan modellen skapa och styra sidopanelsterminaler via de 8 terminal_*-verktygen (av som standard)',
settingsFenceTitle: 'Kontroll av workspace-sökvägar',
settingsFenceDesc: 'På: sidopanelens filfunktioner når bara sökvägar i sessionens workspace (standard); av: vilken fil som helst på värden kan nås — sidans skript får samma räckvidd så länge',
settingsFileIconThemeTitle: 'Filikoner',
settingsFileIconThemeDesc: 'Ikonstil för filträdet och filflikarna (färgtemat laddas vid behov, aldrig vid start)',
settingsFileIconThemeBuiltin: 'Inbyggd (monokrom)',
settingsFileIconThemeBuiltinDesc: 'Monokroma ikoner som följer temats tokens, för vanliga kod-, dokument- och mediatyper',
settingsFileIconThemeColored: 'Färgade varumärkesikoner',
settingsFileIconThemeColoredDesc: 'Varumärkesfärger per typ (t.ex. .tsx → React, package.json → npm); ikonpaketet laddas första gången du aktiverar det',
fenceErrorReason: 'Denna sökväg ligger utanför sessionens workspace och blockerades av kontrollen',
fenceDisableAction: 'Stäng av workspace-kontrollen',
settingsBottomTerminalTitle: 'Öppna automatiskt en terminal vid bottenpanelens första expandering',
+6
View File
@@ -266,6 +266,12 @@ export const th: Record<string, string> = {
settingsToolsDesc: 'เมื่อเปิดใช้ โมเดลสามารถสร้างและควบคุมเทอร์มินัลแถบด้านข้างผ่านเครื่องมือ terminal_* 8 ตัว (ปิดเป็นค่าเริ่มต้น)',
settingsFenceTitle: 'การตรวจสอบเส้นทางเวิร์กสเปซ',
settingsFenceDesc: 'เมื่อเปิด ฟีเจอร์ไฟล์ของแถบข้างเข้าถึงได้เฉพาะเส้นทางในเวิร์กสเปซของเซสชัน (ค่าเริ่มต้น) เมื่อปิด จะเข้าถึงไฟล์ใดก็ได้บนโฮสต์ — สคริปต์ในหน้าก็ได้ขอบเขตเดียวกันตลอดช่วงนั้น',
settingsFileIconThemeTitle: 'ไอคอนไฟล์',
settingsFileIconThemeDesc: 'สไตล์ไอคอนของแผนผังไฟล์และแท็บไฟล์ (ธีมสีจะโหลดเมื่อต้องการ ไม่โหลดตอนเริ่มต้น)',
settingsFileIconThemeBuiltin: 'ในตัว (สีเดียว)',
settingsFileIconThemeBuiltinDesc: 'ไอคอนสีเดียวตามโทเค็นธีม ครอบคลุมชนิดโค้ด / เอกสาร / สื่อทั่วไป',
settingsFileIconThemeColored: 'ไอคอนแบรนด์แบบมีสี',
settingsFileIconThemeColoredDesc: 'สีแบรนด์ตามชนิด (เช่น .tsx → React, package.json → npm) ชุดไอคอนจะโหลดเมื่อเปิดใช้ครั้งแรก',
fenceErrorReason: 'เส้นทางนี้อยู่นอกเวิร์กสเปซของเซสชันและถูกบล็อกโดยการตรวจสอบ',
fenceDisableAction: 'ปิดการตรวจสอบเวิร์กสเปซ',
settingsBottomTerminalTitle: 'เปิดเทอร์มินัลอัตโนมัติเมื่อพาเนลล่างขยายครั้งแรก',
+6
View File
@@ -266,6 +266,12 @@ export const tr: Record<string, string> = {
settingsToolsDesc: 'Etkinleştirildiğinde model, 8 terminal_* aracı üzerinden kenar çubuğu terminalleri oluşturup kullanabilir (varsayılan kapalı)',
settingsFenceTitle: 'Çalışma alanı yol denetimi',
settingsFenceDesc: 'Açıkken kenar çubuğunun dosya işlevleri yalnızca oturum çalışma alanındaki yollara erişir (varsayılan); kapatıldığında ana makinedeki her dosyaya erişilebilir — o süre boyunca sayfadaki betikler de aynı erişimi kazanır',
settingsFileIconThemeTitle: 'Dosya simgeleri',
settingsFileIconThemeDesc: 'Dosya ağacı ve dosya sekmeleri için simge stili (renkli tema isteğe bağlı yüklenir, başlangıçta değil)',
settingsFileIconThemeBuiltin: 'Yerleşik (tek renk)',
settingsFileIconThemeBuiltinDesc: 'Tema belirteçlerine uyan tek renk simgeler; yaygın kod / belge / medya türlerini kapsar',
settingsFileIconThemeColored: 'Renkli marka simgeleri',
settingsFileIconThemeColoredDesc: 'Türe göre marka renkleri (örn. .tsx → React, package.json → npm); simge paketi ilk açılışta yüklenir',
fenceErrorReason: 'Bu yol oturum çalışma alanının dışında ve denetim tarafından engellendi',
fenceDisableAction: 'Çalışma alanı denetimini kapat',
settingsBottomTerminalTitle: 'Alt panelin ilk genişletilmesinde otomatik terminal aç',
+6
View File
@@ -266,6 +266,12 @@ export const vi: Record<string, string> = {
settingsToolsDesc: 'Khi bật, mô hình có thể tạo và điều khiển terminal thanh bên qua 8 công cụ terminal_* (mặc định tắt)',
settingsFenceTitle: 'Kiểm tra đường dẫn workspace',
settingsFenceDesc: 'Khi bật, các tính năng tệp của thanh bên chỉ truy cập đường dẫn trong workspace của phiên (mặc định); khi tắt, mọi tệp trên máy chủ đều có thể truy cập — script trong trang cũng có quyền tương ứng trong thời gian đó',
settingsFileIconThemeTitle: 'Biểu tượng tệp',
settingsFileIconThemeDesc: 'Kiểu biểu tượng cho cây tệp và tab tệp (chủ đề màu được tải khi cần, không tải lúc khởi động)',
settingsFileIconThemeBuiltin: 'Tích hợp (đơn sắc)',
settingsFileIconThemeBuiltinDesc: 'Biểu tượng đơn sắc theo token giao diện, bao phủ các loại mã / tài liệu / media phổ biến',
settingsFileIconThemeColored: 'Biểu tượng thương hiệu nhiều màu',
settingsFileIconThemeColoredDesc: 'Màu thương hiệu theo loại (ví dụ .tsx → React, package.json → npm); gói biểu tượng tải trong lần bật đầu tiên',
fenceErrorReason: 'Đường dẫn này nằm ngoài workspace của phiên và đã bị chặn bởi kiểm tra',
fenceDisableAction: 'Tắt kiểm tra workspace',
settingsBottomTerminalTitle: 'Tự động mở terminal khi panel dưới mở rộng lần đầu',
+6
View File
@@ -281,6 +281,12 @@ export const zhHK: Record<string, string> = {
settingsToolsDesc: '開啟後,模型可透過 terminal_create 等 8 個工具建立並操作側邊欄終端(預設關閉)',
settingsFenceTitle: '工作區路徑檢測',
settingsFenceDesc: '開啟後,側欄的檔案功能僅能存取工作階段工作區內的路徑(預設);關閉後可存取主機上任意檔案——關閉期間頁面內指令碼也將獲得同等存取能力',
settingsFileIconThemeTitle: '檔案圖示',
settingsFileIconThemeDesc: '檔案樹與檔案分頁的圖示風格(彩色主題按需載入,不影響啟動)',
settingsFileIconThemeBuiltin: '內建(單色)',
settingsFileIconThemeBuiltinDesc: '跟隨皮膚權杖的單色圖示,覆蓋常見程式碼 / 文件 / 媒體類型',
settingsFileIconThemeColored: '彩色品牌圖示',
settingsFileIconThemeColoredDesc: '按類型顯示品牌配色圖示(如 .tsx → React、package.json → npm);首次開啟時載入圖示包',
fenceErrorReason: '此路徑在工作階段工作區之外,已被工作區檢測攔截',
fenceDisableAction: '關閉工作區檢測',
settingsBottomTerminalTitle: '底部面板首次展開自動開終端',
+6
View File
@@ -281,6 +281,12 @@ export const zhMO: Record<string, string> = {
settingsToolsDesc: '開啟後,模型可透過 terminal_create 等 8 個工具建立並操作側邊欄終端(預設關閉)',
settingsFenceTitle: '工作區路徑檢測',
settingsFenceDesc: '開啟後,側欄的檔案功能僅能存取工作階段工作區內的路徑(預設);關閉後可存取主機上任意檔案——關閉期間頁面內指令碼也將獲得同等存取能力',
settingsFileIconThemeTitle: '檔案圖示',
settingsFileIconThemeDesc: '檔案樹與檔案分頁的圖示風格(彩色主題按需載入,不影響啟動)',
settingsFileIconThemeBuiltin: '內建(單色)',
settingsFileIconThemeBuiltinDesc: '跟隨皮膚權杖的單色圖示,覆蓋常見程式碼 / 文件 / 媒體類型',
settingsFileIconThemeColored: '彩色品牌圖示',
settingsFileIconThemeColoredDesc: '按類型顯示品牌配色圖示(如 .tsx → React、package.json → npm);首次開啟時載入圖示包',
fenceErrorReason: '此路徑在工作階段工作區之外,已被工作區檢測攔截',
fenceDisableAction: '關閉工作區檢測',
settingsBottomTerminalTitle: '底部面板首次展開自動開終端',
+6
View File
@@ -281,6 +281,12 @@ export const zhTW: Record<string, string> = {
settingsToolsDesc: '開啟後,模型可透過 terminal_create 等 8 個工具建立並操作側邊欄終端(預設關閉)',
settingsFenceTitle: '工作區路徑檢測',
settingsFenceDesc: '開啟後,側欄的檔案功能僅能存取工作階段工作區內的路徑(預設);關閉後可存取主機上任意檔案——關閉期間頁面內指令碼也將獲得同等存取能力',
settingsFileIconThemeTitle: '檔案圖示',
settingsFileIconThemeDesc: '檔案樹與檔案分頁的圖示風格(彩色主題按需載入,不影響啟動)',
settingsFileIconThemeBuiltin: '內建(單色)',
settingsFileIconThemeBuiltinDesc: '跟隨面板權杖的單色圖示,涵蓋常見程式碼 / 文件 / 媒體類型',
settingsFileIconThemeColored: '彩色品牌圖示',
settingsFileIconThemeColoredDesc: '依類型顯示品牌配色圖示(如 .tsx → React、package.json → npm);首次開啟時載入圖示套件',
fenceErrorReason: '此路徑在工作階段工作區之外,已被工作區檢測攔截',
fenceDisableAction: '關閉工作區檢測',
settingsBottomTerminalTitle: '底部面板首次展開自動開終端',
+12
View File
@@ -278,6 +278,12 @@ export const zh = {
settingsToolsDesc: '开启后,模型可通过 terminal_create 等 8 个工具创建并操作侧边栏终端(默认关闭)',
settingsFenceTitle: '工作区路径检测',
settingsFenceDesc: '开启后,侧栏的文件功能仅能访问会话工作区内的路径(默认);关闭后可访问主机上任意文件——关闭期间页面内脚本也将获得同等访问能力',
settingsFileIconThemeTitle: '文件图标',
settingsFileIconThemeDesc: '文件树与文件 tab 的图标风格(彩色主题按需加载,不影响启动)',
settingsFileIconThemeBuiltin: '内置(单色)',
settingsFileIconThemeBuiltinDesc: '跟随皮肤令牌的单色图标,覆盖常见代码 / 文档 / 媒体类型',
settingsFileIconThemeColored: '彩色品牌图标',
settingsFileIconThemeColoredDesc: '按类型显示品牌配色图标(如 .tsx → React、package.json → npm);首次开启时加载图标包',
fenceErrorReason: '此路径在会话工作区之外,已被工作区检测拦截',
fenceDisableAction: '关闭工作区检测',
settingsBottomTerminalTitle: '底部面板首次展开自动开终端',
@@ -727,6 +733,12 @@ export const en: Record<keyof typeof zh, string> = {
settingsToolsDesc: 'When enabled, the model can create and drive sidebar terminals through the 8 terminal_* tools (off by default)',
settingsFenceTitle: 'Workspace path fence',
settingsFenceDesc: 'On, the sidebar\'s file features only reach paths inside the session workspace (default); off, any file on the host is reachable — page scripts gain the same reach while it is off',
settingsFileIconThemeTitle: 'File icons',
settingsFileIconThemeDesc: 'Icon style for the file tree and file tabs (the colored theme loads on demand, never at startup)',
settingsFileIconThemeBuiltin: 'Built-in (monochrome)',
settingsFileIconThemeBuiltinDesc: 'Token-driven monochrome glyphs covering common code / document / media types',
settingsFileIconThemeColored: 'Colored brand icons',
settingsFileIconThemeColoredDesc: 'Per-type brand colors (e.g. .tsx → React, package.json → npm); the icon pack loads the first time you enable it',
fenceErrorReason: 'This path is outside the session workspace and was blocked by the workspace fence',
fenceDisableAction: 'Turn off the workspace fence',
settingsBottomTerminalTitle: 'Auto-open a terminal on the bottom panel\'s first expansion',
+3
View File
@@ -88,6 +88,9 @@ export function parsePrefs(value: unknown): SidebarPrefs {
workspaceFence: typeof record.workspaceFence === 'boolean'
? record.workspaceFence
: SIDEBAR_PREFS_DEFAULTS.workspaceFence,
fileIconTheme: typeof record.fileIconTheme === 'string'
? record.fileIconTheme
: SIDEBAR_PREFS_DEFAULTS.fileIconTheme,
// The title-bar scheme (auto | web | preset | custom). The schema
// declares the field WITHOUT a default, so documents written by older
// plugin versions resolve without it — migrate from the legacy fields:
+1
View File
@@ -128,6 +128,7 @@ export const PrefsSchema: z<SidebarPrefs> = z.object({
editorExplorer: z.boolean().default(false),
changesDiffFloat: z.boolean().default(true),
workspaceFence: z.boolean().default(true),
fileIconTheme: z.string().default('builtin'),
terminalShell: z.string().default(''),
terminalShellArgs: z.string().default(''),
titleBarScheme: z.union([z.const('auto'), z.const('web'), z.const('preset'), z.const('custom')]),
+9
View File
@@ -102,6 +102,14 @@ export interface SidebarPrefs {
* one-click global off + retry.
*/
workspaceFence: boolean
/**
* The file-icon theme (v0.19.0+). `'builtin'` (default) is the monochrome
* VSCodicons glyph map every surface already uses; `'colored'` swaps in the
* optional brand-colored dataset, fetched from the lazy `file-icons` chunk
* only when selected (an unknown id behaves as `'builtin'`). The switch
* lives under the files tab's gear in the Side card settings.
*/
fileIconTheme: string
/**
* The shell the UI and agent terminals spawn (absolute path or bare
* executable name). Empty (default) keeps the legacy resolution order:
@@ -280,6 +288,7 @@ export const SIDEBAR_PREFS_DEFAULTS: SidebarPrefs = {
editorExplorer: false,
changesDiffFloat: true,
workspaceFence: true,
fileIconTheme: 'builtin',
terminalShell: '',
terminalShellArgs: '',
titleBarScheme: 'auto',
+1 -1
View File
@@ -110,7 +110,7 @@ describe('built-in tab registrations', () => {
it('the editor tab declares its merged-mode (embedded file tree) setting', () => {
const { service } = setup()
const toggles = service.getTab('editor')?.settings?.toggles ?? []
expect(toggles.map(t => t.key)).toEqual(['editorExplorer', 'workspaceFence'])
expect(toggles.map(t => t.key)).toEqual(['editorExplorer', 'workspaceFence', 'fileIconTheme'])
expect(toggles[0]?.title).toBeDefined()
expect(toggles[0]?.desc).toBeDefined()
// The merged mode is an iconed select (merged vs separate), not a switch.
+1 -1
View File
@@ -150,6 +150,6 @@ describe('/sidebar/bundle route', () => {
})
it('exports the chunk allowlist (mirror of src/client/chunk-loader.ts)', () => {
expect([...CHUNK_NAMES]).toEqual(['terminal', 'editor', 'mermaid', 'locale'])
expect([...CHUNK_NAMES]).toEqual(['terminal', 'editor', 'mermaid', 'locale', 'file-icons'])
})
})
+176
View File
@@ -0,0 +1,176 @@
/**
* The optional colored file-icon theme: the lazy-chunk loader that turns the
* `fileIconTheme` pref into a register/dispose of the colored dataset, and
* the dataset's own registration through the public `registerFileIcon` API.
*
* The loader tests drive a fake chunk (no fetch); the dataset tests use the
* real chunk module against a real service, asserting representative rules
* from each table (extension / exact file name / directory name).
*/
import { describe, it, expect, vi, afterEach } from 'vitest'
import type { ReactElement, ReactNode } from 'react'
import { SiNpm, SiReact } from 'react-icons/si'
import { VscFolder, VscFolderOpened } from 'react-icons/vsc'
// Mock browser globals (SidebarStore.reduce → schedulePersist uses window.setTimeout)
const g = globalThis as Record<string, unknown>
if (g.window === undefined) {
g.window = {
clearTimeout: () => {},
setTimeout: (_fn: () => void) => 0,
innerWidth: 1024,
}
}
if (g.localStorage === undefined) {
g.localStorage = { getItem: () => null, setItem: () => {} }
}
import { registerChunkForTests, resetChunks } from '../src/client/chunk-loader.ts'
import { createBetterSidebarService } from '../src/client/service.ts'
import { createSidebarStore } from '../src/client/state.ts'
import {
createFileIconThemeLoader,
FILE_ICON_THEME_BUILTIN,
FILE_ICON_THEME_COLORED,
} from '../src/client/file-icon-theme.ts'
import { registerColoredFileIcons } from '../src/client/chunks/file-icons.tsx'
/** The component type of a rendered icon element (identity of the glyph). */
const glyphOf = (node: ReactNode): unknown => (node as ReactElement).type
/** The inline color of a rendered icon element (undefined when untinted). */
const colorOf = (node: ReactNode): unknown => (node as ReactElement).props.style?.color
/** Flush the queued promise chain (loadChunk → loader's `.then`). */
const settle = async (): Promise<void> => { await new Promise((resolve) => { setTimeout(resolve, 0) }) }
afterEach(() => { resetChunks() })
describe('file-icon theme loader (pref → lazy chunk → registerFileIcon)', () => {
it('registers the dataset when the colored theme is selected, and disposes on builtin', async () => {
const service = createBetterSidebarService(createSidebarStore())
let registrations = 0
let disposals = 0
registerChunkForTests('file-icons', async () => ({
registerColoredFileIcons: (target: typeof service) => {
expect(target).toBe(service)
registrations += 1
return () => { disposals += 1 }
},
}))
const loader = createFileIconThemeLoader(service)
expect(service.getFileIcons()).toHaveLength(0)
loader.set(FILE_ICON_THEME_COLORED)
await settle()
expect(registrations).toBe(1)
loader.set(FILE_ICON_THEME_BUILTIN)
expect(disposals).toBe(1)
loader.set(FILE_ICON_THEME_COLORED)
await settle()
expect(registrations).toBe(2)
loader.dispose()
expect(disposals).toBe(2)
})
it('treats an unknown pref value as the built-in theme', async () => {
const service = createBetterSidebarService(createSidebarStore())
let registrations = 0
registerChunkForTests('file-icons', async () => ({
registerColoredFileIcons: () => { registrations += 1; return () => {} },
}))
const loader = createFileIconThemeLoader(service)
loader.set('no-such-theme')
await settle()
expect(registrations).toBe(0)
})
it('drops a chunk load that lands after the pref moved back to builtin', async () => {
const service = createBetterSidebarService(createSidebarStore())
let registrations = 0
let release: (() => void) | undefined
registerChunkForTests('file-icons', () => new Promise((resolve) => {
release = () => resolve({ registerColoredFileIcons: () => { registrations += 1; return () => {} } })
}))
const loader = createFileIconThemeLoader(service)
loader.set(FILE_ICON_THEME_COLORED)
loader.set(FILE_ICON_THEME_BUILTIN)
release?.()
await settle()
expect(registrations).toBe(0)
})
it('does not re-register on a repeated set of the same value', async () => {
const service = createBetterSidebarService(createSidebarStore())
let registrations = 0
registerChunkForTests('file-icons', async () => ({
registerColoredFileIcons: () => { registrations += 1; return () => {} },
}))
const loader = createFileIconThemeLoader(service)
loader.set(FILE_ICON_THEME_COLORED)
loader.set(FILE_ICON_THEME_COLORED)
await settle()
expect(registrations).toBe(1)
})
it('logs and keeps the built-in glyphs when the chunk fails to load', async () => {
const service = createBetterSidebarService(createSidebarStore())
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
try {
registerChunkForTests('file-icons', async () => { throw new Error('offline') })
const loader = createFileIconThemeLoader(service)
loader.set(FILE_ICON_THEME_COLORED)
await settle()
expect(errorSpy).toHaveBeenCalled()
expect(service.getFileIcons()).toHaveLength(0)
} finally {
errorSpy.mockRestore()
}
})
it('logs and keeps the built-in glyphs when the chunk exports no registrar', async () => {
const service = createBetterSidebarService(createSidebarStore())
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
try {
registerChunkForTests('file-icons', async () => ({}))
const loader = createFileIconThemeLoader(service)
loader.set(FILE_ICON_THEME_COLORED)
await settle()
expect(errorSpy).toHaveBeenCalled()
expect(service.getFileIcons()).toHaveLength(0)
} finally {
errorSpy.mockRestore()
}
})
})
describe('colored dataset registration (563 rules through registerFileIcon)', () => {
it('registers the extension, file-name and directory rules and disposes them', () => {
const service = createBetterSidebarService(createSidebarStore())
const dispose = registerColoredFileIcons(service)
expect(service.getFileIcons().length).toBeGreaterThan(100)
// Extension rule: .tsx → React blue, and it outranks the built-in glyph.
const tsx = service.fileIcon('/w/App.tsx', 14)
expect(glyphOf(tsx)).toBe(SiReact)
expect(colorOf(tsx)).toBe('#61DAFB')
// Exact file name rule: package.json → npm red (name beats the .json ext).
const pkg = service.fileIcon('/w/package.json', 14)
expect(glyphOf(pkg)).toBe(SiNpm)
expect(colorOf(pkg)).toBe('#CB3837')
// Directory rule: node_modules is tinted, an unnamed dir keeps the glyph.
const nodeModules = service.folderIcon('/w/node_modules', false, 14)
expect(glyphOf(nodeModules)).toBe(VscFolder)
expect(colorOf(nodeModules)).toBe('#CB3837')
expect(glyphOf(service.folderIcon('/w/node_modules', true, 14))).toBe(VscFolderOpened)
expect(glyphOf(service.folderIcon('/w/zzz-unnamed-dir', false, 14))).toBe(VscFolder)
expect(colorOf(service.folderIcon('/w/zzz-unnamed-dir', false, 14))).toBeUndefined()
// An extension the dataset does not cover still falls back to the built-in.
expect(glyphOf(service.fileIcon('/w/data.unknownext', 14))).toBeDefined()
dispose()
expect(service.getFileIcons()).toHaveLength(0)
expect(colorOf(service.fileIcon('/w/App.tsx', 14))).toBeUndefined()
})
})
+1 -1
View File
@@ -71,7 +71,7 @@ function bundleId(file: string): string {
}
/** The lazy chunk bundle names (mirror of src/bundle-route.ts CHUNK_NAMES). */
const CHUNK_FILES = ['terminal', 'editor', 'mermaid'].map(name => `lib/client-${name}.js`)
const CHUNK_FILES = ['terminal', 'editor', 'mermaid', 'file-icons'].map(name => `lib/client-${name}.js`)
/** The global registry slot a built chunk script assigns (its factory key). */
function chunkSlot(file: string): string {
+1 -1
View File
@@ -97,6 +97,6 @@ describe('dsh-better-sidebar plugin export shape', () => {
const overridden = (PrefsSchema as unknown as {
(input: Record<string, unknown> | undefined): Record<string, unknown>
})({ openByDefault: false, defaultWidthPercent: 45 })
expect(overridden).toEqual({ openByDefault: false, defaultWidthPercent: 45, autoOpenSubagent: true, autoOpenJobs: true, agentTerminalTools: false, agentOpenTools: false, bottomPanelAutoTerminal: true, terminalFontFamily: '', terminalFontSize: 13, interceptOpenPath: true, editorExplorer: false, workspaceFence: true, terminalShell: '', terminalShellArgs: '', titleBarCompat: false, titleBarStripPx: 40, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, browserInterceptHttp: true, browserInterceptHttps: false, browserAllowedLoopback: '', changesDiffFloat: true, tabsEnabled: {}, viewersEnabled: {}, pluginSettings: {} })
expect(overridden).toEqual({ openByDefault: false, defaultWidthPercent: 45, autoOpenSubagent: true, autoOpenJobs: true, agentTerminalTools: false, agentOpenTools: false, bottomPanelAutoTerminal: true, terminalFontFamily: '', terminalFontSize: 13, interceptOpenPath: true, editorExplorer: false, workspaceFence: true, fileIconTheme: 'builtin', terminalShell: '', terminalShellArgs: '', titleBarCompat: false, titleBarStripPx: 40, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, browserInterceptHttp: true, browserInterceptHttps: false, browserAllowedLoopback: '', changesDiffFloat: true, tabsEnabled: {}, viewersEnabled: {}, pluginSettings: {} })
})
})
+18 -5
View File
@@ -44,6 +44,7 @@ describe('side card preferences', () => {
interceptOpenPath: true,
editorExplorer: false, changesDiffFloat: true,
workspaceFence: true,
fileIconTheme: 'builtin',
terminalShell: '',
terminalShellArgs: '',
titleBarScheme: 'auto',
@@ -78,6 +79,7 @@ describe('side card preferences', () => {
interceptOpenPath: true,
editorExplorer: false, changesDiffFloat: true,
workspaceFence: true,
fileIconTheme: 'builtin',
terminalShell: '',
terminalShellArgs: '',
titleBarScheme: 'auto',
@@ -112,6 +114,7 @@ describe('side card preferences', () => {
interceptOpenPath: true,
editorExplorer: false, changesDiffFloat: true,
workspaceFence: true,
fileIconTheme: 'builtin',
terminalShell: '',
terminalShellArgs: '',
titleBarScheme: 'auto',
@@ -182,6 +185,16 @@ describe('side card preferences', () => {
expect((await loadPrefs(wire({ workspaceFence: false }))).workspaceFence).toBe(false)
})
it('defaults fileIconTheme to builtin; only a stored string survives', async () => {
// The colored theme is opt-in: absent or malformed keeps the monochrome
// built-in glyphs (and never fetches the icon chunk).
expect((await loadPrefs(wire({}))).fileIconTheme).toBe('builtin')
expect((await loadPrefs(wire({ fileIconTheme: 7 }))).fileIconTheme).toBe('builtin')
expect((await loadPrefs(wire({ fileIconTheme: 'colored' }))).fileIconTheme).toBe('colored')
// An unknown id is preserved here and treated as builtin by the loader.
expect((await loadPrefs(wire({ fileIconTheme: 'ghost' }))).fileIconTheme).toBe('ghost')
})
it('defaults the title-bar scheme to the conservative auto with no preset or custom CSS', async () => {
// Absent or malformed → auto (plain web keeps the untouched layout).
expect((await loadPrefs(wire({}))).titleBarScheme).toBe('auto')
@@ -282,9 +295,9 @@ describe('side card preferences', () => {
const store = createSidebarStore()
// Node environment: no window → the width falls back to PANEL_DEFAULT,
// while the open flag still follows the preference.
store.setPrefs({ openByDefault: false, defaultWidthPercent: 45, autoOpenSubagent: true, autoOpenJobs: true, agentTerminalTools: false, agentOpenTools: false, bottomPanelAutoTerminal: true, terminalFontFamily: '', terminalFontSize: 13, interceptOpenPath: true, editorExplorer: true, changesDiffFloat: true, workspaceFence: true, terminalShell: '', terminalShellArgs: '', titleBarScheme: 'auto', titleBarPresetId: '', customCss: '', titleBarCompat: false, titleBarStripPx: 40, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, browserInterceptHttp: true, browserInterceptHttps: false, browserAllowedLoopback: "", tabsEnabled: {}, viewersEnabled: {}, pluginSettings: {} })
store.setPrefs({ openByDefault: false, defaultWidthPercent: 45, autoOpenSubagent: true, autoOpenJobs: true, agentTerminalTools: false, agentOpenTools: false, bottomPanelAutoTerminal: true, terminalFontFamily: '', terminalFontSize: 13, interceptOpenPath: true, editorExplorer: true, changesDiffFloat: true, workspaceFence: true, fileIconTheme: 'builtin', terminalShell: '', terminalShellArgs: '', titleBarScheme: 'auto', titleBarPresetId: '', customCss: '', titleBarCompat: false, titleBarStripPx: 40, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, browserInterceptHttp: true, browserInterceptHttps: false, browserAllowedLoopback: "", tabsEnabled: {}, viewersEnabled: {}, pluginSettings: {} })
store.setSession('fresh-session')
expect(store.getPrefs()).toEqual({ openByDefault: false, defaultWidthPercent: 45, autoOpenSubagent: true, autoOpenJobs: true, agentTerminalTools: false, agentOpenTools: false, bottomPanelAutoTerminal: true, terminalFontFamily: '', terminalFontSize: 13, interceptOpenPath: true, editorExplorer: true, changesDiffFloat: true, workspaceFence: true, terminalShell: '', terminalShellArgs: '', titleBarScheme: 'auto', titleBarPresetId: '', customCss: '', titleBarCompat: false, titleBarStripPx: 40, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, browserInterceptHttp: true, browserInterceptHttps: false, browserAllowedLoopback: "", tabsEnabled: {}, viewersEnabled: {}, pluginSettings: {} })
expect(store.getPrefs()).toEqual({ openByDefault: false, defaultWidthPercent: 45, autoOpenSubagent: true, autoOpenJobs: true, agentTerminalTools: false, agentOpenTools: false, bottomPanelAutoTerminal: true, terminalFontFamily: '', terminalFontSize: 13, interceptOpenPath: true, editorExplorer: true, changesDiffFloat: true, workspaceFence: true, fileIconTheme: 'builtin', terminalShell: '', terminalShellArgs: '', titleBarScheme: 'auto', titleBarPresetId: '', customCss: '', titleBarCompat: false, titleBarStripPx: 40, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, browserInterceptHttp: true, browserInterceptHttps: false, browserAllowedLoopback: "", tabsEnabled: {}, viewersEnabled: {}, pluginSettings: {} })
const snapshot = store.getSnapshot()
expect(snapshot.sessionId).toBe('fresh-session')
expect(snapshot.state?.panelOpen).toBe(false)
@@ -320,7 +333,7 @@ describe('side card preferences', () => {
it('skips the default seed tab when the editor (files window) type is disabled', () => {
const store = createSidebarStore()
store.setPrefs({ openByDefault: true, defaultWidthPercent: 30, autoOpenSubagent: true, autoOpenJobs: true, agentTerminalTools: false, agentOpenTools: false, bottomPanelAutoTerminal: true, terminalFontFamily: '', terminalFontSize: 13, interceptOpenPath: true, editorExplorer: true, changesDiffFloat: true, workspaceFence: true, terminalShell: '', terminalShellArgs: '', titleBarScheme: 'auto', titleBarPresetId: '', customCss: '', titleBarCompat: false, titleBarStripPx: 40, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, browserInterceptHttp: true, browserInterceptHttps: false, browserAllowedLoopback: "", tabsEnabled: { editor: false }, viewersEnabled: {}, pluginSettings: {} })
store.setPrefs({ openByDefault: true, defaultWidthPercent: 30, autoOpenSubagent: true, autoOpenJobs: true, agentTerminalTools: false, agentOpenTools: false, bottomPanelAutoTerminal: true, terminalFontFamily: '', terminalFontSize: 13, interceptOpenPath: true, editorExplorer: true, changesDiffFloat: true, workspaceFence: true, fileIconTheme: 'builtin', terminalShell: '', terminalShellArgs: '', titleBarScheme: 'auto', titleBarPresetId: '', customCss: '', titleBarCompat: false, titleBarStripPx: 40, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, browserInterceptHttp: true, browserInterceptHttps: false, browserAllowedLoopback: "", tabsEnabled: { editor: false }, viewersEnabled: {}, pluginSettings: {} })
store.setSession('no-editor')
const state = store.getSnapshot().state!
const tabs = allLeaves(state.splits).flatMap(leaf => leaf.tabs)
@@ -330,7 +343,7 @@ describe('side card preferences', () => {
// editorExplorer modes.
for (const editorExplorer of [true, false]) {
const openStore = createSidebarStore()
openStore.setPrefs({ openByDefault: true, defaultWidthPercent: 30, autoOpenSubagent: true, autoOpenJobs: true, agentTerminalTools: false, agentOpenTools: false, bottomPanelAutoTerminal: true, terminalFontFamily: '', terminalFontSize: 13, interceptOpenPath: true, editorExplorer, changesDiffFloat: true, workspaceFence: true, terminalShell: '', terminalShellArgs: '', titleBarScheme: 'auto', titleBarPresetId: '', customCss: '', titleBarCompat: false, titleBarStripPx: 40, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, browserInterceptHttp: true, browserInterceptHttps: false, browserAllowedLoopback: "", tabsEnabled: {}, viewersEnabled: {}, pluginSettings: {} })
openStore.setPrefs({ openByDefault: true, defaultWidthPercent: 30, autoOpenSubagent: true, autoOpenJobs: true, agentTerminalTools: false, agentOpenTools: false, bottomPanelAutoTerminal: true, terminalFontFamily: '', terminalFontSize: 13, interceptOpenPath: true, editorExplorer, changesDiffFloat: true, workspaceFence: true, fileIconTheme: 'builtin', terminalShell: '', terminalShellArgs: '', titleBarScheme: 'auto', titleBarPresetId: '', customCss: '', titleBarCompat: false, titleBarStripPx: 40, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, browserInterceptHttp: true, browserInterceptHttps: false, browserAllowedLoopback: "", tabsEnabled: {}, viewersEnabled: {}, pluginSettings: {} })
openStore.setSession(`with-editor-${editorExplorer}`)
const openTabs = allLeaves(openStore.getSnapshot().state!.splits).flatMap(leaf => leaf.tabs)
expect(openTabs.map(tab => tab.type)).toEqual(['editor'])
@@ -340,7 +353,7 @@ describe('side card preferences', () => {
it('seeds the empty editor home tab (files window) in both editorExplorer modes', () => {
for (const editorExplorer of [true, false]) {
const store = createSidebarStore()
store.setPrefs({ openByDefault: true, defaultWidthPercent: 30, autoOpenSubagent: true, autoOpenJobs: true, agentTerminalTools: false, agentOpenTools: false, bottomPanelAutoTerminal: true, terminalFontFamily: '', terminalFontSize: 13, interceptOpenPath: true, editorExplorer, changesDiffFloat: true, workspaceFence: true, terminalShell: '', terminalShellArgs: '', titleBarScheme: 'auto', titleBarPresetId: '', customCss: '', titleBarCompat: false, titleBarStripPx: 40, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, browserInterceptHttp: true, browserInterceptHttps: false, browserAllowedLoopback: "", tabsEnabled: {}, viewersEnabled: {}, pluginSettings: {} })
store.setPrefs({ openByDefault: true, defaultWidthPercent: 30, autoOpenSubagent: true, autoOpenJobs: true, agentTerminalTools: false, agentOpenTools: false, bottomPanelAutoTerminal: true, terminalFontFamily: '', terminalFontSize: 13, interceptOpenPath: true, editorExplorer, changesDiffFloat: true, workspaceFence: true, fileIconTheme: 'builtin', terminalShell: '', terminalShellArgs: '', titleBarScheme: 'auto', titleBarPresetId: '', customCss: '', titleBarCompat: false, titleBarStripPx: 40, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, browserInterceptHttp: true, browserInterceptHttps: false, browserAllowedLoopback: "", tabsEnabled: {}, viewersEnabled: {}, pluginSettings: {} })
store.setSession(`fresh-${editorExplorer}`)
const tabs = allLeaves(store.getSnapshot().state!.splits).flatMap(leaf => leaf.tabs)
expect(tabs).toHaveLength(1)
+1
View File
@@ -950,6 +950,7 @@ describe('side card settings routes', () => {
interceptOpenPath: true,
editorExplorer: false,
workspaceFence: true,
fileIconTheme: 'builtin',
terminalShell: '',
terminalShellArgs: '',
titleBarCompat: false,
+38
View File
@@ -13,6 +13,8 @@
*/
// @vitest-environment jsdom
import { afterEach, describe, expect, it } from 'vitest'
import { readFileSync, readdirSync } from 'node:fs'
import { resolve } from 'node:path'
import { colorAlpha, effectiveTokenValue, tokenValue } from '../src/client/theme.ts'
afterEach(() => {
@@ -80,3 +82,39 @@ describe('effectiveTokenValue', () => {
expect(effectiveTokenValue('--probe')).toBe('')
})
})
/**
* Skin contract (guide §12): every visual value rides a `--dsw-alias-*` /
* `--dsw-font-*` / `--ds-*` token. The ONE documented exception is the
* optional colored icon theme: brand colors are identity, not chrome, so
* they are hardcoded — but only inside the lazy `file-icons` chunk, which
* the core bundle must never import. This guards that boundary.
*/
// jsdom has no file:// import.meta.url; vitest runs from the repo root.
const ROOT = process.cwd()
describe('skin contract boundary for hardcoded icon colors', () => {
const coreIconModules = ['src/client/file-icons.tsx', 'src/client/file-icon-theme.ts']
it('the core icon modules carry no color literals', () => {
for (const file of coreIconModules) {
const source = readFileSync(resolve(ROOT, file), 'utf8')
expect(source, file).not.toMatch(/#[0-9a-fA-F]{3,8}\b/)
expect(source, file).not.toMatch(/\brgba?\(/)
}
})
it('only the lazy chunk may carry the colored dataset, and the core never imports it statically', () => {
const chunk = readFileSync(resolve(ROOT, 'src/client/chunks/file-icons.tsx'), 'utf8')
expect(chunk).toMatch(/#[0-9a-fA-F]{6}/)
const clientDir = resolve(ROOT, 'src/client')
const offenders: string[] = []
for (const entry of readdirSync(clientDir, { recursive: true, withFileTypes: true })) {
if (!entry.isFile() || !/\.tsx?$/.test(entry.name)) continue
const path = resolve(entry.parentPath, entry.name)
if (path.includes('/chunks/')) continue
if (/from '[^']*chunks\/file-icons/.test(readFileSync(path, 'utf8'))) offenders.push(path)
}
expect(offenders).toEqual([])
})
})
+6 -1
View File
@@ -211,6 +211,11 @@ function chunkBundle(name: string): UserConfig {
inputOptions: {
resolve: {
conditionNames: ['browser', 'import', 'require', 'default'],
// The icon chunks import a handful of glyphs from the two react-icons
// packs; without this alias the pack's unshakeable CJS entry pulls in
// the whole set (6.4 MB for the file-icons chunk). Same reason the
// core bundle pins it.
...(name === 'file-icons' ? { alias: REACT_ICONS_ESM_ALIAS } : {}),
},
},
noExternal: (id: string) => (CLIENT_EXTERNALS.includes(id) ? undefined : true),
@@ -324,7 +329,7 @@ function makeCssPlugin(pluginId: string): BuildPlugin {
}
/** The lazy chunk names (keep in sync with src/bundle-route.ts CHUNK_NAMES). */
const CHUNKS = ['terminal', 'editor', 'mermaid', 'locale']
const CHUNKS = ['terminal', 'editor', 'mermaid', 'locale', 'file-icons']
export default [
{