feat: 终端二级设置支持自定义字体(字体族 + 9–32px 字号,实时生效)
- SidebarPrefs 新增 terminalFontFamily(空 = 跟随主题等宽字体)/ terminalFontSize(9–32,默认 13),PrefsSchema 与 parsePrefs 同步校验 - 声明式设置 API 扩展:SidebarSettingToggle 支持 type: 'switch' | 'text' | 'number'(缺省 switch,向后兼容)+ min/max/placeholder/unit - 终端卡片齿轮弹窗新增「终端字体」「终端字号」两行:blur/Enter 提交、number 钳制、提交失败回滚草稿 - TerminalView 经 store 订阅实时应用字体(options + re-fit + resize),懒加载 chunk 无新增依赖 - 新增 resolveTerminalFont 纯函数(回退链:用户字体 → 主题 token → 内置等宽栈)并单测 - 版本 0.11.0,README/AGENTS.md/设计文档同步
This commit is contained in:
@@ -127,15 +127,27 @@ interface TabDescriptor {
|
|||||||
createTab?: (state: SidebarState) => { tab: SidebarTab; patch?: Partial<SidebarState> } | null
|
createTab?: (state: SidebarState) => { tab: SidebarTab; patch?: Partial<SidebarState> } | null
|
||||||
/**
|
/**
|
||||||
* 声明式设置(v0.4.1+):每个注册的 tab 都会在 Side card 设置页获得一行
|
* 声明式设置(v0.4.1+):每个注册的 tab 都会在 Side card 设置页获得一行
|
||||||
* 开关(图标 + 标题 + 类型 id),`settings.toggles` 在其行下追加嵌套开关,
|
* 开关(图标 + 标题 + 类型 id),`settings.toggles` 在其行下追加嵌套设置行,
|
||||||
* 绑定 SidebarPrefs 字段。嵌套开关仅父级启用时显示。
|
* 绑定 SidebarPrefs 字段。嵌套设置仅父级启用时显示(v0.11.0 起行控件不限于
|
||||||
|
* 布尔开关:`type: 'switch' | 'text' | 'number'`,缺省 'switch';text/number
|
||||||
|
* 行 blur/Enter 提交,number 行按 min/max 钳制,unit 渲染单位后缀)。
|
||||||
*/
|
*/
|
||||||
settings?: {
|
settings?: {
|
||||||
toggles?: readonly {
|
toggles?: readonly {
|
||||||
/** SidebarPrefs 字段名(内置键:'autoOpenSubagent' / 'agentTerminalTools' / 'htmlViewerNoSandbox' / 'htmlViewerDefaultUnsafe' / 'browserNoSandbox' / 'browserInterceptLinks') */
|
/** SidebarPrefs 字段名(内置键:'autoOpenSubagent' / 'agentTerminalTools' / 'terminalFontFamily' / 'htmlViewerNoSandbox' / 'htmlViewerDefaultUnsafe' / 'browserNoSandbox' / 'browserInterceptLinks') */
|
||||||
key: string
|
key: string
|
||||||
title: string | (() => string)
|
title: string | (() => string)
|
||||||
desc?: string | (() => string)
|
desc?: string | (() => string)
|
||||||
|
/** 行控件类型;缺省 'switch'(向后兼容)。 */
|
||||||
|
type?: 'switch' | 'text' | 'number'
|
||||||
|
/** number 行的提交钳制下限。 */
|
||||||
|
min?: number
|
||||||
|
/** number 行的提交钳制上限。 */
|
||||||
|
max?: number
|
||||||
|
/** text 行的输入占位符。 */
|
||||||
|
placeholder?: string
|
||||||
|
/** 输入框后的单位后缀(如 'px')。 */
|
||||||
|
unit?: string
|
||||||
}[]
|
}[]
|
||||||
}
|
}
|
||||||
/** 渲染函数 */
|
/** 渲染函数 */
|
||||||
@@ -384,7 +396,7 @@ interface BetterSidebarService {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
> **声明式设置(v0.4.1+)**:每个注册的 tab/viewer 自动出现在 DSH 设置页「侧边卡片」分区的清单里——响应式网格中的**小卡片**(图标 + 标题 + 类型 id + **高亮 = 启用**,勾选徽标钉在卡片最右端,viewer 卡片还显示扩展名),开关持久化到 `SidebarPrefs.tabsEnabled / viewersEnabled`(开放 map,缺省 = 启用)。关闭语义:tab 从 `+` 菜单消失、`openTab` 拒绝新开、子代理自动展开 / agent 终端自动补 tab 等派生流程停止,**已打开的 tab 保留**;viewer 被 `matchFileViewer` 跳过,文件落到下一个匹配。`settings.toggles` 声明的相关设置(如子代理的 `autoOpenSubagent`)通过卡片右下角的齿轮按钮在**原生弹窗**中编辑(复选框行),父级卡片关闭时齿轮隐藏;**key 必须是宿主 PrefsSchema 的字段**(内置键:`autoOpenSubagent` / `agentTerminalTools` / `htmlViewerNoSandbox` / `htmlViewerDefaultUnsafe` / `browserNoSandbox` / `browserInterceptLinks`),外部插件的自定义键会被 settings seam 丢弃。
|
> **声明式设置(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+)——父级卡片关闭时齿轮隐藏;**key 必须是宿主 PrefsSchema 的字段**(内置键:`autoOpenSubagent` / `agentTerminalTools` / `terminalFontFamily` / `terminalFontSize` / `htmlViewerNoSandbox` / `htmlViewerDefaultUnsafe` / `browserNoSandbox` / `browserInterceptLinks`),外部插件的自定义键会被 settings seam 丢弃。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -494,7 +506,7 @@ better-sidebar 自己的内置 tab 和 viewer 就是参考实现("吃狗粮"
|
|||||||
|
|
||||||
- **`src/client/builtins/`**:7 个内置 tab(explorer/git/subagent/terminal/browser/editor/diff)+ 9 个内置 viewer(image/pdf/docx/xlsx/pptx/markdown/html/code/binary-download)的注册代码(tabs.tsx / viewers.tsx / index.ts)
|
- **`src/client/builtins/`**:7 个内置 tab(explorer/git/subagent/terminal/browser/editor/diff)+ 9 个内置 viewer(image/pdf/docx/xlsx/pptx/markdown/html/code/binary-download)的注册代码(tabs.tsx / viewers.tsx / index.ts)
|
||||||
- **`src/client/service.ts`**:`BetterSidebarService` 接口 + `createBetterSidebarService` 工厂实现
|
- **`src/client/service.ts`**:`BetterSidebarService` 接口 + `createBetterSidebarService` 工厂实现
|
||||||
- **`src/client/SideCardSection.tsx`**:声明式设置页(注册表驱动清单 + `settings.toggles` 嵌套开关 + 开关持久化)
|
- **`src/client/SideCardSection.tsx`**:声明式设置页(注册表驱动清单 + `settings.toggles` 嵌套设置行:switch/text/number + 持久化)
|
||||||
- **`tests/service.spec.ts`**:注册表生命周期 / 匹配算法 / dedupe / createTab / 启用态 gating 测试
|
- **`tests/service.spec.ts`**:注册表生命周期 / 匹配算法 / dedupe / createTab / 启用态 gating 测试
|
||||||
- **`tests/builtins.spec.ts`**:内置注册清单断言(7 tab + 9 viewer + 声明式元数据)
|
- **`tests/builtins.spec.ts`**:内置注册清单断言(7 tab + 9 viewer + 声明式元数据)
|
||||||
- **`docs/plans/2026-08-11-service-registry-design.md`** / **`docs/plans/2026-08-11-declarative-sidebar-settings-design.md`**:设计文档(含实施偏差记录)
|
- **`docs/plans/2026-08-11-service-registry-design.md`** / **`docs/plans/2026-08-11-declarative-sidebar-settings-design.md`**:设计文档(含实施偏差记录)
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ https://github.com/user-attachments/assets/23187822-047e-45cc-b480-fe997bd55b86
|
|||||||
- **📝 编辑与预览**:CodeMirror 6 多语言高亮 + Ctrl/Cmd+S 原子保存;图片 / Markdown(预览/编辑切换)/ HTML(沙箱 iframe 预览,相对资源可加载)/ PDF / Word / Excel / PPT 内联预览,切换 Tab 不丢草稿
|
- **📝 编辑与预览**:CodeMirror 6 多语言高亮 + Ctrl/Cmd+S 原子保存;图片 / Markdown(预览/编辑切换)/ HTML(沙箱 iframe 预览,相对资源可加载)/ PDF / Word / Excel / PPT 内联预览,切换 Tab 不丢草稿
|
||||||
- **⚡ 客户端懒加载**:Office / 终端 / 代码编辑器等重依赖按需分块加载——启动只拉 ~325KB 核心,打开 .xlsx 才拉 Univer(~20MB)、打开 .docx 只拉 docx 预览器、打开终端才拉 xterm;首次打开短暂 loading 后即用(详见 `docs/plans/2026-08-12-lazy-chunks-design.md`)
|
- **⚡ 客户端懒加载**:Office / 终端 / 代码编辑器等重依赖按需分块加载——启动只拉 ~325KB 核心,打开 .xlsx 才拉 Univer(~20MB)、打开 .docx 只拉 docx 预览器、打开终端才拉 xterm;首次打开短暂 loading 后即用(详见 `docs/plans/2026-08-12-lazy-chunks-design.md`)
|
||||||
- **🌐 浏览器**:内嵌网页浏览 tab(多开),后退/前进/刷新 +「在浏览器中打开」;页面在**沙箱 iframe** 中运行(不透明源:无法访问界面数据与本地文件,拒绝 localhost 等本机地址),界面实时显示沙箱状态、可临时解锁(关闭时红色警示);被站点拒绝嵌入(X-Frame-Options)时显示原因面板;聊天/界面里的 http(s) 外链默认在侧边栏打开(侧边栏折叠时自动展开面板)
|
- **🌐 浏览器**:内嵌网页浏览 tab(多开),后退/前进/刷新 +「在浏览器中打开」;页面在**沙箱 iframe** 中运行(不透明源:无法访问界面数据与本地文件,拒绝 localhost 等本机地址),界面实时显示沙箱状态、可临时解锁(关闭时红色警示);被站点拒绝嵌入(X-Frame-Options)时显示原因面板;聊天/界面里的 http(s) 外链默认在侧边栏打开(侧边栏折叠时自动展开面板)
|
||||||
- **💻 终端**:xterm.js + node-pty 真实 shell(每会话 3 个 UI 上限)、Tab 保活重连回放;可选为模型注入 8 个 `terminal_*` 工具
|
- **💻 终端**:xterm.js + node-pty 真实 shell(每会话 3 个 UI 上限)、Tab 保活重连回放;可选为模型注入 8 个 `terminal_*` 工具;**自定义字体**(终端卡片二级设置:字体族 + 9–32px 字号,实时生效,留空跟随主题等宽字体)
|
||||||
- **🌿 Git 面板**:真 diff + VSCode 式 diff tab、懒加载历史、右键暂存/放弃/提交/还原/捡取
|
- **🌿 Git 面板**:真 diff + VSCode 式 diff tab、懒加载历史、右键暂存/放弃/提交/还原/捡取
|
||||||
- **🧩 后台任务页**:主会话完整 agent 拓扑、点击直达执行记录、实时工具调用轮询、新子代理自动展开;**同页显示后台任务**(当前树全部后台任务,bash/pwsh 类型徽标 + 退出码,点击查看实时输出——自动跟随底部、非消费 peek,不干扰模型的 `job_output`;两击确认可强制终止)
|
- **🧩 后台任务页**:主会话完整 agent 拓扑、点击直达执行记录、实时工具调用轮询、新子代理自动展开;**同页显示后台任务**(当前树全部后台任务,bash/pwsh 类型徽标 + 退出码,点击查看实时输出——自动跟随底部、非消费 peek,不干扰模型的 `job_output`;两击确认可强制终止)
|
||||||
- **🪟 底部面板**:独立的第二个工作台(与右侧栏同类的标签页),只挤占中间 Agent 输出区、不覆盖左右侧边栏;**首次展开自动开一个新终端**(终端卡片二级设置可关);右上角 x 一键折叠
|
- **🪟 底部面板**:独立的第二个工作台(与右侧栏同类的标签页),只挤占中间 Agent 输出区、不覆盖左右侧边栏;**首次展开自动开一个新终端**(终端卡片二级设置可关);右上角 x 一键折叠
|
||||||
|
|||||||
+1
-1
@@ -22,7 +22,7 @@ https://github.com/user-attachments/assets/23187822-047e-45cc-b480-fe997bd55b86
|
|||||||
- **📝 Edit & Preview**: CodeMirror 6 multi-language highlighting + Ctrl/Cmd+S atomic save; inline preview for images / Markdown (preview/edit toggle) / HTML (sandboxed iframe preview, relative resources loadable) / PDF / Word / Excel / PPT; drafts survive tab switches
|
- **📝 Edit & Preview**: CodeMirror 6 multi-language highlighting + Ctrl/Cmd+S atomic save; inline preview for images / Markdown (preview/edit toggle) / HTML (sandboxed iframe preview, relative resources loadable) / PDF / Word / Excel / PPT; drafts survive tab switches
|
||||||
- **⚡ Client-side Lazy Loading**: heavy dependencies (Office / terminal / code editor) are chunked and loaded on demand — only ~325KB core is fetched at startup; Univer (~20MB) is fetched only when opening .xlsx, the docx viewer only when opening .docx, xterm only when opening a terminal; brief loading on first open, then instant (see `docs/plans/2026-08-12-lazy-chunks-design.md`)
|
- **⚡ Client-side Lazy Loading**: heavy dependencies (Office / terminal / code editor) are chunked and loaded on demand — only ~325KB core is fetched at startup; Univer (~20MB) is fetched only when opening .xlsx, the docx viewer only when opening .docx, xterm only when opening a terminal; brief loading on first open, then instant (see `docs/plans/2026-08-12-lazy-chunks-design.md`)
|
||||||
- **🌐 Browser**: embedded web browsing tabs (multiple), back/forward/refresh + "Open in browser"; pages run in a **sandboxed iframe** (opaque origin: cannot access UI data or local files, rejects localhost and other local addresses); the UI shows sandbox status live and can be temporarily unlocked (red warning when off); sites that refuse embedding (X-Frame-Options) show a reason panel; http(s) links in chat/UI open in the sidebar by default (panel auto-expands when collapsed)
|
- **🌐 Browser**: embedded web browsing tabs (multiple), back/forward/refresh + "Open in browser"; pages run in a **sandboxed iframe** (opaque origin: cannot access UI data or local files, rejects localhost and other local addresses); the UI shows sandbox status live and can be temporarily unlocked (red warning when off); sites that refuse embedding (X-Frame-Options) show a reason panel; http(s) links in chat/UI open in the sidebar by default (panel auto-expands when collapsed)
|
||||||
- **💻 Terminal**: xterm.js + node-pty real shell (max 3 UI instances per session), tab keep-alive with reconnect/replay; optionally injects 8 `terminal_*` tools for the model
|
- **💻 Terminal**: xterm.js + node-pty real shell (max 3 UI instances per session), tab keep-alive with reconnect/replay; optionally injects 8 `terminal_*` tools for the model; **custom font** (terminal card's secondary settings: font family + 9–32px size, applied live, empty follows the theme's monospace font)
|
||||||
- **🌿 Git Panel**: real diff + VSCode-style diff tabs, lazy-loaded history, right-click to stage/discard/commit/revert/pick
|
- **🌿 Git Panel**: real diff + VSCode-style diff tabs, lazy-loaded history, right-click to stage/discard/commit/revert/pick
|
||||||
- **🧩 Background Tasks**: full agent topology of the main session, click-through to execution records, live tool-call polling, auto-expand for new subagents; background tasks shown on the same page (all background tasks in the current tree, bash/pwsh type badges + exit codes, click to view live output — auto-follows the bottom, non-consuming peek that doesn't disturb the model's `job_output`; double-click confirm to force-kill)
|
- **🧩 Background Tasks**: full agent topology of the main session, click-through to execution records, live tool-call polling, auto-expand for new subagents; background tasks shown on the same page (all background tasks in the current tree, bash/pwsh type badges + exit codes, click to view live output — auto-follows the bottom, non-consuming peek that doesn't disturb the model's `job_output`; double-click confirm to force-kill)
|
||||||
- **🪟 Bottom Panel**: an independent second workbench (same kind of tabs as the right sidebar) that only squeezes the middle Agent output area without covering the left/right sidebars; auto-opens a new terminal on first expand (can be disabled in the terminal card's secondary settings); one-click collapse via the top-right x
|
- **🪟 Bottom Panel**: an independent second workbench (same kind of tabs as the right sidebar) that only squeezes the middle Agent output area without covering the left/right sidebars; auto-opens a new terminal on first expand (can be disabled in the terminal card's secondary settings); one-click collapse via the top-right x
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
# 终端自定义字体(设置页二级设置)设计
|
||||||
|
|
||||||
|
**日期**:2026-08-14
|
||||||
|
**状态**:已实施(本文档含实施偏差记录)
|
||||||
|
**目标版本**:v0.11.0
|
||||||
|
|
||||||
|
## 1. 目标
|
||||||
|
|
||||||
|
在 DSH 设置页「侧边卡片」分区中,终端 tab 卡片的二级设置弹窗(齿轮按钮,README 所称"终端卡片二级设置")新增两行:
|
||||||
|
|
||||||
|
1. **终端字体**(text 输入):自定义 CSS font-family stack,如 `"JetBrains Mono", monospace`;留空 = 跟随主题等宽字体(`--ds-font-family-code` token)。
|
||||||
|
2. **终端字号**(number 输入):9–32px,默认 13,带 `px` 后缀。
|
||||||
|
|
||||||
|
配置持久化到 `SidebarPrefs`(`dsh-better-sidebar` 命名空间,host 侧 PrefsSchema 校验),对**所有已打开终端实时生效**(UI tab 与 agent tab 共用 TerminalView):设置页提交 → `store.setPrefs` 通知 → xterm `options.fontFamily/fontSize` 更新 + 重排网格。
|
||||||
|
|
||||||
|
## 2. 非目标
|
||||||
|
|
||||||
|
- 不在终端视图内加菜单/按钮(用户明确选择仅设置页齿轮弹窗)。
|
||||||
|
- 不做 per-tab / per-terminal 隔离(全局偏好,与 SidebarPrefs 现有语义一致)。
|
||||||
|
- 不自动给字体族加引号、不做字体合法性嗅探(用户负责合法 CSS font-family;placeholder 示范格式)。
|
||||||
|
- 不改 pty / WS / 终端连接生命周期:字体变化不重连、不重启 shell。
|
||||||
|
|
||||||
|
## 3. 现状回顾
|
||||||
|
|
||||||
|
- 终端描述符(`builtins/tabs.tsx`)的 `settings.toggles` 只支持**布尔开关**(`SidebarSettingToggle` 无类型字段),`FeatureSettingsRows`(`SideCardSection.tsx`)只渲染 Switch 行。
|
||||||
|
- `SidebarPrefs` / `PrefsSchema` / `parsePrefs` 三处同步维护(`prefs-shared.ts` / `config.ts` / `client/prefs.ts`)。
|
||||||
|
- `TerminalView` 硬编码 `fontSize: 13` + `fontFamily: tokenValue('--ds-font-family-code') || 内置栈`,且不响应偏好变化。
|
||||||
|
|
||||||
|
## 4. 设计
|
||||||
|
|
||||||
|
### 4.1 偏好扩展(`prefs-shared.ts` / `config.ts` / `client/prefs.ts`)
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface SidebarPrefs {
|
||||||
|
// …
|
||||||
|
terminalFontFamily: string // '' = 跟随主题等宽字体
|
||||||
|
terminalFontSize: number // 9–32,默认 13
|
||||||
|
}
|
||||||
|
|
||||||
|
// prefs-shared.ts
|
||||||
|
export const TERMINAL_FONT_SIZE_MIN = 9
|
||||||
|
export const TERMINAL_FONT_SIZE_MAX = 32
|
||||||
|
export const TERMINAL_FONT_SIZE_DEFAULT = 13
|
||||||
|
export function clampTerminalFontSize(value: number): number // round + clamp(仿 clampWidthPercent)
|
||||||
|
```
|
||||||
|
|
||||||
|
- `PrefsSchema`:`terminalFontFamily: z.string().default('')`、`terminalFontSize: z.number().step(1).min(9).max(32).default(13)` —— 旧文档解析自动补默认,无迁移。
|
||||||
|
- `parsePrefs`:family 需 string 否则默认;size 需有限数并 `clampTerminalFontSize` 否则默认。
|
||||||
|
|
||||||
|
### 4.2 声明式 API 扩展(`client/service.ts`,向后兼容)
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type SidebarSettingToggleType = 'switch' | 'text' | 'number'
|
||||||
|
|
||||||
|
interface SidebarSettingToggle {
|
||||||
|
key: string
|
||||||
|
title: string | (() => string)
|
||||||
|
desc?: string | (() => string)
|
||||||
|
type?: SidebarSettingToggleType // 缺省 'switch',外部插件现有用法不变
|
||||||
|
min?: number // number 行的下限(提交时钳制)
|
||||||
|
max?: number // number 行的上限
|
||||||
|
placeholder?: string // text 行的占位符
|
||||||
|
unit?: string // 输入框后的单位后缀(如 'px')
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 终端描述符声明(`builtins/tabs.tsx`)
|
||||||
|
|
||||||
|
`settings.toggles` 追加两行:`{ key: 'terminalFontFamily', type: 'text', placeholder: … }` 与 `{ key: 'terminalFontSize', type: 'number', min: 9, max: 32, unit: 'px' }`(标题/描述走 locales)。
|
||||||
|
|
||||||
|
### 4.4 设置行渲染(`SideCardSection.tsx` + CSS)
|
||||||
|
|
||||||
|
- `FeatureSettingsRows` 按 `toggle.type` 分支:`switch` 走现有 Switch 行;`text`/`number` 走新 `TypedRow`(局部 draft state,`Input` 原语,blur/Enter 提交,number 行带 unit 后缀)。
|
||||||
|
- `TypedRow` 的 `key` 含**已提交值**:提交失败 → prefs 回滚 → key 变化 → 行重挂载为存储值(输入中 draft 不因 key 变化被打断,因为 key 只随已提交值变)。
|
||||||
|
- 父级 `onCommitSetting(toggle, raw): string`:number 解析+钳制(非法/空 → 走 `Number('')=0` 钳到下限,与宽度行 `commitWidth` 先例一致);text 原样;乐观 `setPrefs` → `commit` → 失败 `applyOutcome` 回滚(复用既有 `applyPref` 管道,原 `togglePref` 更名)。
|
||||||
|
|
||||||
|
### 4.5 终端消费(`client/terminal-font.ts` 新模块 + `TerminalView.tsx`)
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// terminal-font.ts(纯函数,可单测)
|
||||||
|
export const DEFAULT_TERMINAL_FONT_FAMILY = '"SF Mono", Menlo, Consolas, "Liberation Mono", monospace'
|
||||||
|
export function resolveTerminalFont(prefs, themeFontFamily):
|
||||||
|
{ fontFamily: 非空用户值 ?? themeFontFamily ?? 内置栈, fontSize: clampTerminalFontSize(prefs.terminalFontSize) }
|
||||||
|
```
|
||||||
|
|
||||||
|
- `TerminalView` 构造 Terminal 时用解析值;主 effect 内 `store.subscribe` 回调 diff `term.options.fontFamily/fontSize`,变化则更新 options + `fit.fit()` + `sendResize()`(网格尺寸随字体变化);cleanup 退订。懒加载 chunk 无新增依赖(store 为运行时 prop)。
|
||||||
|
|
||||||
|
### 4.6 i18n(`locales.ts`)
|
||||||
|
|
||||||
|
zh/en 各新增:`settingsFontFamilyTitle/Desc/Placeholder`、`settingsFontSizeTitle/Desc/Suffix`(px)。
|
||||||
|
|
||||||
|
## 5. 测试
|
||||||
|
|
||||||
|
- `tests/unit.spec.ts`:`parsePrefs`/`loadPrefs` 新字段(合法 string 直通、越界钳制 9/32、非 number 回退、缺省默认);全等字面量补字段。
|
||||||
|
- `tests/plugin-shape.spec.ts`:PrefsSchema 默认值 + overridden 全等。
|
||||||
|
- `tests/builtins.spec.ts`:终端 toggles keys = 4 个,后两个 type/min/max 断言。
|
||||||
|
- `tests/smoke.spec.ts`:settings.get 全默认字面量补字段。
|
||||||
|
- `tests/side-card-section.spec.tsx`(SSR):text/number 行渲染(value/min/max/placeholder/unit,非 checkbox)。
|
||||||
|
- `tests/side-card-section-rows.spec.tsx`(新,jsdom + createRoot):blur 提交回调、number 钳制、空输入钳到下限、提交失败回滚草稿(key 重挂载)。
|
||||||
|
- `tests/terminal-font.spec.ts`(新):`resolveTerminalFont` 回退链 + `clampTerminalFontSize`。
|
||||||
|
|
||||||
|
## 6. 限制与取舍
|
||||||
|
|
||||||
|
- 外部插件的自定义 toggle key 照旧被 host PrefsSchema 校验丢弃(现有 seam 行为);`type: 'text'/'number'` 是公开 API 扩展,外部插件可用但 key 仍须是宿主 schema 字段。
|
||||||
|
- number 输入的"非法值"在浏览器里只会以空串出现(number input 拒绝非数字字符),因此 `Number('')=0 → 钳到下限` 是唯一可达路径,与宽度行先例一致。
|
||||||
|
- 字号实时生效依赖 xterm 的 options setter + re-fit;若未来 xterm 版本行为变化,回退方案是重建 Terminal(不推荐,会丢滚动缓冲)。
|
||||||
|
|
||||||
|
## 7. 实施偏差记录
|
||||||
|
|
||||||
|
- 计划中的"非法输入回退当前存储值"用例在实现中发现不可达(浏览器 number input 的值清洗),改为断言"空输入钳到下限"(与宽度行一致)。
|
||||||
|
- `togglePref` 更名为 `applyPref`(它本就接受任意 patch,不只是布尔翻转)。
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"id": "dsh-external/dsh-better-sidebar",
|
"id": "dsh-external/dsh-better-sidebar",
|
||||||
"version": "0.10.3",
|
"version": "0.11.0",
|
||||||
"main": "./lib/index.js",
|
"main": "./lib/index.js",
|
||||||
"description": "VSCode 风格右侧侧边栏:文件资源管理器 / 编辑器 / 终端 / Git / 浏览器,按会话隔离。暴露服务供其他插件注册侧边栏页面和文件预览器",
|
"description": "VSCode 风格右侧侧边栏:文件资源管理器 / 编辑器 / 终端 / Git / 浏览器,按会话隔离。暴露服务供其他插件注册侧边栏页面和文件预览器",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dsh-better-sidebar",
|
"name": "dsh-better-sidebar",
|
||||||
"version": "0.10.3",
|
"version": "0.11.0",
|
||||||
"description": "DSH web plugin: a VSCode-like right sidebar (explorer / editor / terminal / git / browser), isolated per conversation session. Exposes a service for other plugins to register sidebar tabs and file viewers.",
|
"description": "DSH web plugin: a VSCode-like right sidebar (explorer / editor / terminal / git / browser), isolated per conversation session. Exposes a service for other plugins to register sidebar tabs and file viewers.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
@@ -337,6 +337,17 @@
|
|||||||
width: 76px;
|
width: 76px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Text/number rows in the settings popup: the free-form input (e.g. the
|
||||||
|
terminal font family) gets a generous width; numeric rows (font size)
|
||||||
|
keep the compact width of the general number inputs. */
|
||||||
|
.typedInput {
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typedInputNumber {
|
||||||
|
width: 76px;
|
||||||
|
}
|
||||||
|
|
||||||
.suffix {
|
.suffix {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 22px;
|
line-height: 22px;
|
||||||
|
|||||||
+111
-20
@@ -141,37 +141,105 @@ function Switch(props: {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* The body of a feature's secondary settings popup: one row (title/desc +
|
* The body of a feature's secondary settings popup: one row (title/desc +
|
||||||
* custom switch) per declared toggle. Extracted so the rows are testable
|
* control) per declared setting. Switches render the custom switch; text and
|
||||||
|
* number rows render a free-form / numeric input committed on blur/Enter
|
||||||
|
* (clamped to the declared min/max). Extracted so the rows are testable
|
||||||
* without opening the Modal (the Modal portal renders only while open).
|
* without opening the Modal (the Modal portal renders only while open).
|
||||||
*/
|
*/
|
||||||
export function FeatureSettingsRows(props: {
|
export function FeatureSettingsRows(props: {
|
||||||
toggles: readonly SidebarSettingToggle[]
|
toggles: readonly SidebarSettingToggle[]
|
||||||
prefs: SidebarPrefs
|
prefs: SidebarPrefs
|
||||||
onToggle: (toggle: SidebarSettingToggle, next: boolean) => void
|
onToggle: (toggle: SidebarSettingToggle, next: boolean) => void
|
||||||
|
/** Commit one text/number row; returns the canonical value the row should
|
||||||
|
* display (clamped for numbers, the current pref when the input is
|
||||||
|
* invalid). Optional: rows with no handler keep their draft. */
|
||||||
|
onCommit?: (toggle: SidebarSettingToggle, raw: string) => string
|
||||||
}) {
|
}) {
|
||||||
const { toggles, prefs, onToggle } = props
|
const { toggles, prefs, onToggle, onCommit } = props
|
||||||
return (
|
return (
|
||||||
<div className={css.popupRows}>
|
<div className={css.popupRows}>
|
||||||
{toggles.map(toggle => {
|
{toggles.map(toggle => {
|
||||||
const title = textOf(toggle.title)
|
const title = textOf(toggle.title)
|
||||||
|
if ((toggle.type ?? 'switch') === 'switch') {
|
||||||
|
return (
|
||||||
|
<div key={toggle.key} className={css.popupRow}>
|
||||||
|
<span className={css.rowText}>
|
||||||
|
<span className={css.title}>{title}</span>
|
||||||
|
{textOf(toggle.desc) !== '' && <span className={css.desc}>{textOf(toggle.desc)}</span>}
|
||||||
|
</span>
|
||||||
|
<Switch
|
||||||
|
label={title}
|
||||||
|
checked={prefBool(prefs, toggle.key)}
|
||||||
|
onChange={(next) => { onToggle(toggle, next) }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const value = String((prefs as unknown as Record<string, unknown>)[toggle.key] ?? '')
|
||||||
|
// Keyed by the committed value: a failed commit reverts prefs, the
|
||||||
|
// key changes, and the row remounts with the stored value (typing
|
||||||
|
// never changes the key, so mid-edit drafts survive re-renders).
|
||||||
return (
|
return (
|
||||||
<div key={toggle.key} className={css.popupRow}>
|
<TypedRow
|
||||||
<span className={css.rowText}>
|
key={`${toggle.key}:${value}`}
|
||||||
<span className={css.title}>{title}</span>
|
toggle={toggle}
|
||||||
{textOf(toggle.desc) !== '' && <span className={css.desc}>{textOf(toggle.desc)}</span>}
|
title={title}
|
||||||
</span>
|
value={value}
|
||||||
<Switch
|
onCommit={onCommit}
|
||||||
label={title}
|
/>
|
||||||
checked={prefBool(prefs, toggle.key)}
|
|
||||||
onChange={(next) => { onToggle(toggle, next) }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One text/number row: a controlled input whose draft is local state,
|
||||||
|
* committed on blur/Enter through the parent's onCommit. The parent's
|
||||||
|
* canonical return is adopted (clamped numbers, stored value for invalid
|
||||||
|
* input); a `unit` suffix renders after the input (e.g. 'px').
|
||||||
|
*/
|
||||||
|
function TypedRow(props: {
|
||||||
|
toggle: SidebarSettingToggle
|
||||||
|
title: string
|
||||||
|
value: string
|
||||||
|
onCommit?: (toggle: SidebarSettingToggle, raw: string) => string
|
||||||
|
}) {
|
||||||
|
const { toggle, title, value, onCommit } = props
|
||||||
|
const [draft, setDraft] = useState(value)
|
||||||
|
const commit = (): void => {
|
||||||
|
const canonical = onCommit?.(toggle, draft) ?? draft
|
||||||
|
setDraft(canonical)
|
||||||
|
}
|
||||||
|
const number = toggle.type === 'number'
|
||||||
|
return (
|
||||||
|
<div className={css.popupRow}>
|
||||||
|
<span className={css.rowText}>
|
||||||
|
<span className={css.title}>{title}</span>
|
||||||
|
{textOf(toggle.desc) !== '' && <span className={css.desc}>{textOf(toggle.desc)}</span>}
|
||||||
|
</span>
|
||||||
|
<span className={css.control}>
|
||||||
|
<Input
|
||||||
|
type={number ? 'number' : 'text'}
|
||||||
|
className={number ? css.typedInputNumber : css.typedInput}
|
||||||
|
value={draft}
|
||||||
|
min={toggle.min}
|
||||||
|
max={toggle.max}
|
||||||
|
step={1}
|
||||||
|
placeholder={toggle.placeholder}
|
||||||
|
aria-label={title}
|
||||||
|
onChange={event => { setDraft(event.currentTarget.value) }}
|
||||||
|
onBlur={commit}
|
||||||
|
onKeyDown={event => {
|
||||||
|
if (event.key === 'Enter') event.currentTarget.blur()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{toggle.unit !== undefined && <span className={css.suffix}>{toggle.unit}</span>}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render the Side card preferences section.
|
* Render the Side card preferences section.
|
||||||
* @param props - composed slot props (runtime share + injected store/service).
|
* @param props - composed slot props (runtime share + injected store/service).
|
||||||
@@ -251,8 +319,8 @@ export function SideCardSection({ store, service }: SideCardSectionProps) {
|
|||||||
setWidthDraft(String(settled.defaultWidthPercent))
|
setWidthDraft(String(settled.defaultWidthPercent))
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Optimistically flip one boolean pref, then commit (revert on failure). */
|
/** Optimistically apply one pref patch, then commit (revert on failure). */
|
||||||
const togglePref = (patch: Record<string, unknown>): void => {
|
const applyPref = (patch: Record<string, unknown>): void => {
|
||||||
const previous = prefs
|
const previous = prefs
|
||||||
setPrefs({ ...previous, ...patch } as SidebarPrefs)
|
setPrefs({ ...previous, ...patch } as SidebarPrefs)
|
||||||
setError(null)
|
setError(null)
|
||||||
@@ -260,22 +328,44 @@ export function SideCardSection({ store, service }: SideCardSectionProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onToggle = (next: boolean): void => {
|
const onToggle = (next: boolean): void => {
|
||||||
togglePref({ openByDefault: next })
|
applyPref({ openByDefault: next })
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Flip one per-tab enable switch (merge into the tabsEnabled map). */
|
/** Flip one per-tab enable switch (merge into the tabsEnabled map). */
|
||||||
const onToggleTab = (id: string, next: boolean): void => {
|
const onToggleTab = (id: string, next: boolean): void => {
|
||||||
togglePref({ tabsEnabled: { ...prefs.tabsEnabled, [id]: next } })
|
applyPref({ tabsEnabled: { ...prefs.tabsEnabled, [id]: next } })
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Flip one per-viewer enable switch (merge into the viewersEnabled map). */
|
/** Flip one per-viewer enable switch (merge into the viewersEnabled map). */
|
||||||
const onToggleViewer = (id: string, next: boolean): void => {
|
const onToggleViewer = (id: string, next: boolean): void => {
|
||||||
togglePref({ viewersEnabled: { ...prefs.viewersEnabled, [id]: next } })
|
applyPref({ viewersEnabled: { ...prefs.viewersEnabled, [id]: next } })
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Flip one declaratively-declared toggle (a SidebarPrefs boolean field). */
|
/** Flip one declaratively-declared toggle (a SidebarPrefs boolean field). */
|
||||||
const onToggleSetting = (toggle: SidebarSettingToggle, next: boolean): void => {
|
const onToggleSetting = (toggle: SidebarSettingToggle, next: boolean): void => {
|
||||||
togglePref({ [toggle.key]: next })
|
applyPref({ [toggle.key]: next })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Commit one declaratively-declared text/number row. Numbers are parsed
|
||||||
|
* and clamped to the toggle's declared min/max (an unparsable input falls
|
||||||
|
* back to the CURRENT stored value, mirroring the width row); text rows
|
||||||
|
* persist as-is (empty is meaningful, e.g. the theme-default font).
|
||||||
|
* Returns the canonical value the row should display.
|
||||||
|
*/
|
||||||
|
const onCommitSetting = (toggle: SidebarSettingToggle, raw: string): string => {
|
||||||
|
if (toggle.type === 'number') {
|
||||||
|
const parsed = Number(raw)
|
||||||
|
const fallback = String((prefs as unknown as Record<string, unknown>)[toggle.key] ?? '')
|
||||||
|
if (!Number.isFinite(parsed)) return fallback
|
||||||
|
let clamped = Math.round(parsed)
|
||||||
|
if (toggle.min !== undefined) clamped = Math.max(toggle.min, clamped)
|
||||||
|
if (toggle.max !== undefined) clamped = Math.min(toggle.max, clamped)
|
||||||
|
applyPref({ [toggle.key]: clamped })
|
||||||
|
return String(clamped)
|
||||||
|
}
|
||||||
|
applyPref({ [toggle.key]: raw })
|
||||||
|
return raw
|
||||||
}
|
}
|
||||||
|
|
||||||
const commitWidth = (): void => {
|
const commitWidth = (): void => {
|
||||||
@@ -397,7 +487,7 @@ export function SideCardSection({ store, service }: SideCardSectionProps) {
|
|||||||
<Switch
|
<Switch
|
||||||
label={t('settingsOpenPathTitle')}
|
label={t('settingsOpenPathTitle')}
|
||||||
checked={prefs.interceptOpenPath}
|
checked={prefs.interceptOpenPath}
|
||||||
onChange={(next) => { togglePref({ interceptOpenPath: next }) }}
|
onChange={(next) => { applyPref({ interceptOpenPath: next }) }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -476,6 +566,7 @@ export function SideCardSection({ store, service }: SideCardSectionProps) {
|
|||||||
toggles={settingsFor.settings?.toggles ?? []}
|
toggles={settingsFor.settings?.toggles ?? []}
|
||||||
prefs={prefs}
|
prefs={prefs}
|
||||||
onToggle={onToggleSetting}
|
onToggle={onToggleSetting}
|
||||||
|
onCommit={onCommitSetting}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import { t } from './locales.ts'
|
|||||||
import type { SessionScope } from './api.ts'
|
import type { SessionScope } from './api.ts'
|
||||||
import { agentUuidOf, isAgentTabId, type SidebarStore } from './state.ts'
|
import { agentUuidOf, isAgentTabId, type SidebarStore } from './state.ts'
|
||||||
import { isDarkScheme, subscribeColorScheme, tokenValue } from './theme.ts'
|
import { isDarkScheme, subscribeColorScheme, tokenValue } from './theme.ts'
|
||||||
|
import { resolveTerminalFont } from './terminal-font.ts'
|
||||||
import css from './sidebar.module.css'
|
import css from './sidebar.module.css'
|
||||||
|
|
||||||
/** How many consecutive unreasoned failures before showing the error banner. */
|
/** How many consecutive unreasoned failures before showing the error banner. */
|
||||||
@@ -82,10 +83,13 @@ export function TerminalView(props: { scope: SessionScope; tabId: string; store:
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const host = hostRef.current
|
const host = hostRef.current
|
||||||
if (host === null) return
|
if (host === null) return
|
||||||
|
// The custom font prefs (side card settings, terminal card) resolve at
|
||||||
|
// mount; store changes re-apply them live below.
|
||||||
|
const font = resolveTerminalFont(store.getPrefs(), tokenValue('--ds-font-family-code'))
|
||||||
const term = new Terminal({
|
const term = new Terminal({
|
||||||
cursorBlink: true,
|
cursorBlink: true,
|
||||||
fontSize: 13,
|
fontSize: font.fontSize,
|
||||||
fontFamily: tokenValue('--ds-font-family-code') || '"SF Mono", Menlo, Consolas, "Liberation Mono", monospace',
|
fontFamily: font.fontFamily,
|
||||||
allowTransparency: true,
|
allowTransparency: true,
|
||||||
convertEol: false,
|
convertEol: false,
|
||||||
scrollback: 4000,
|
scrollback: 4000,
|
||||||
@@ -185,11 +189,31 @@ export function TerminalView(props: { scope: SessionScope; tabId: string; store:
|
|||||||
})
|
})
|
||||||
observer.observe(host)
|
observer.observe(host)
|
||||||
|
|
||||||
|
// Custom font prefs (the terminal card's secondary settings) apply LIVE:
|
||||||
|
// on any store change re-resolve and diff the two options, re-fitting
|
||||||
|
// when they moved (the grid dimensions may change with the font). The
|
||||||
|
// subscribe fires on every store change (tabs, panels…), so the diff is
|
||||||
|
// what keeps this cheap.
|
||||||
|
const fontSub = store.subscribe(() => {
|
||||||
|
const next = resolveTerminalFont(store.getPrefs(), tokenValue('--ds-font-family-code'))
|
||||||
|
if (next.fontFamily !== term.options.fontFamily || next.fontSize !== term.options.fontSize) {
|
||||||
|
term.options.fontFamily = next.fontFamily
|
||||||
|
term.options.fontSize = next.fontSize
|
||||||
|
try {
|
||||||
|
fit.fit()
|
||||||
|
sendResize()
|
||||||
|
} catch {
|
||||||
|
// The terminal may be mid-dispose; ignore.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
connect()
|
connect()
|
||||||
return () => {
|
return () => {
|
||||||
closed = true
|
closed = true
|
||||||
window.clearTimeout(retry)
|
window.clearTimeout(retry)
|
||||||
observer.disconnect()
|
observer.disconnect()
|
||||||
|
fontSub()
|
||||||
schemeSub()
|
schemeSub()
|
||||||
inputSub.dispose()
|
inputSub.dispose()
|
||||||
// The close frame tells the host the owning tab is GONE (immediate
|
// The close frame tells the host the owning tab is GONE (immediate
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { DiffTab } from '../DiffTab.tsx'
|
|||||||
import { SubagentView } from '../SubagentView.tsx'
|
import { SubagentView } from '../SubagentView.tsx'
|
||||||
import { BrowserView } from '../BrowserView.tsx'
|
import { BrowserView } from '../BrowserView.tsx'
|
||||||
import { IconTerminalOutline16, IconDiffOutline16, IconGlobeOutline16 } from '../icons.tsx'
|
import { IconTerminalOutline16, IconDiffOutline16, IconGlobeOutline16 } from '../icons.tsx'
|
||||||
|
import { TERMINAL_FONT_SIZE_MAX, TERMINAL_FONT_SIZE_MIN } from '../../prefs-shared.ts'
|
||||||
import type { ComponentType } from 'react'
|
import type { ComponentType } from 'react'
|
||||||
import type { SessionScope } from '../api.ts'
|
import type { SessionScope } from '../api.ts'
|
||||||
import type { SidebarStore } from '../state.ts'
|
import type { SidebarStore } from '../state.ts'
|
||||||
@@ -137,10 +138,11 @@ export function builtinTabs(ctx: Context): readonly TabDescriptor[] {
|
|||||||
icon: (size: number) => <IconTerminalOutline16 size={size} />,
|
icon: (size: number) => <IconTerminalOutline16 size={size} />,
|
||||||
order: 40,
|
order: 40,
|
||||||
available: (_ctx, _scope, state) => uiTerminalCount(state) < TERMINAL_LIMIT,
|
available: (_ctx, _scope, state) => uiTerminalCount(state) < TERMINAL_LIMIT,
|
||||||
// Declarative settings: the model-facing terminal tools switch and the
|
// Declarative settings: the model-facing terminal tools switch, the
|
||||||
// bottom-panel first-expansion auto-terminal switch render under this
|
// bottom-panel first-expansion auto-terminal switch, and the custom
|
||||||
// row in the Side card settings page (the Terminal page's own related
|
// font family/size rows render under this card in the Side card
|
||||||
// settings; the host gates the toolset on the tools one independently).
|
// settings page (the host gates the toolset on the tools one
|
||||||
|
// independently; the font rows apply live to every terminal).
|
||||||
settings: {
|
settings: {
|
||||||
toggles: [{
|
toggles: [{
|
||||||
key: 'agentTerminalTools',
|
key: 'agentTerminalTools',
|
||||||
@@ -150,6 +152,20 @@ export function builtinTabs(ctx: Context): readonly TabDescriptor[] {
|
|||||||
key: 'bottomPanelAutoTerminal',
|
key: 'bottomPanelAutoTerminal',
|
||||||
title: () => t('settingsBottomTerminalTitle'),
|
title: () => t('settingsBottomTerminalTitle'),
|
||||||
desc: () => t('settingsBottomTerminalDesc'),
|
desc: () => t('settingsBottomTerminalDesc'),
|
||||||
|
}, {
|
||||||
|
key: 'terminalFontFamily',
|
||||||
|
type: 'text',
|
||||||
|
title: () => t('settingsFontFamilyTitle'),
|
||||||
|
desc: () => t('settingsFontFamilyDesc'),
|
||||||
|
placeholder: t('settingsFontFamilyPlaceholder'),
|
||||||
|
}, {
|
||||||
|
key: 'terminalFontSize',
|
||||||
|
type: 'number',
|
||||||
|
title: () => t('settingsFontSizeTitle'),
|
||||||
|
desc: () => t('settingsFontSizeDesc'),
|
||||||
|
min: TERMINAL_FONT_SIZE_MIN,
|
||||||
|
max: TERMINAL_FONT_SIZE_MAX,
|
||||||
|
unit: 'px',
|
||||||
}],
|
}],
|
||||||
},
|
},
|
||||||
createTab: (state) => {
|
createTab: (state) => {
|
||||||
|
|||||||
@@ -136,6 +136,12 @@ export const zh = {
|
|||||||
settingsToolsDesc: '开启后,模型可通过 terminal_create 等 8 个工具创建并操作侧边栏终端(默认关闭)',
|
settingsToolsDesc: '开启后,模型可通过 terminal_create 等 8 个工具创建并操作侧边栏终端(默认关闭)',
|
||||||
settingsBottomTerminalTitle: '底部面板首次展开自动开终端',
|
settingsBottomTerminalTitle: '底部面板首次展开自动开终端',
|
||||||
settingsBottomTerminalDesc: '每次会话中第一次展开底部面板时,尝试在底部面板自动打开一个新终端标签(终端数量上限仍会限制;默认开启)',
|
settingsBottomTerminalDesc: '每次会话中第一次展开底部面板时,尝试在底部面板自动打开一个新终端标签(终端数量上限仍会限制;默认开启)',
|
||||||
|
settingsFontFamilyTitle: '终端字体',
|
||||||
|
settingsFontFamilyDesc: '自定义终端字体族(CSS font-family,如 "JetBrains Mono", monospace;留空跟随主题等宽字体)',
|
||||||
|
settingsFontFamilyPlaceholder: '"JetBrains Mono", monospace',
|
||||||
|
settingsFontSizeTitle: '终端字号',
|
||||||
|
settingsFontSizeDesc: '终端字号(9–32,默认 13)',
|
||||||
|
settingsFontSizeSuffix: 'px',
|
||||||
settingsTabsTitle: '侧边栏内容',
|
settingsTabsTitle: '侧边栏内容',
|
||||||
settingsViewersTitle: '文件预览',
|
settingsViewersTitle: '文件预览',
|
||||||
settingsGeneralTitle: '常规',
|
settingsGeneralTitle: '常规',
|
||||||
@@ -340,6 +346,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)',
|
settingsToolsDesc: 'When enabled, the model can create and drive sidebar terminals through the 8 terminal_* tools (off by default)',
|
||||||
settingsBottomTerminalTitle: 'Auto-open a terminal on the bottom panel\'s first expansion',
|
settingsBottomTerminalTitle: 'Auto-open a terminal on the bottom panel\'s first expansion',
|
||||||
settingsBottomTerminalDesc: 'When the bottom panel is expanded for the first time in a session, try to open a fresh terminal tab there (the terminal quota still applies; on by default)',
|
settingsBottomTerminalDesc: 'When the bottom panel is expanded for the first time in a session, try to open a fresh terminal tab there (the terminal quota still applies; on by default)',
|
||||||
|
settingsFontFamilyTitle: 'Terminal font family',
|
||||||
|
settingsFontFamilyDesc: 'Custom terminal font family (a CSS font-family stack like "JetBrains Mono", monospace; leave empty to follow the theme\'s monospace font)',
|
||||||
|
settingsFontFamilyPlaceholder: '"JetBrains Mono", monospace',
|
||||||
|
settingsFontSizeTitle: 'Terminal font size',
|
||||||
|
settingsFontSizeDesc: 'Terminal font size in px (9–32, default 13)',
|
||||||
|
settingsFontSizeSuffix: 'px',
|
||||||
settingsTabsTitle: 'Sidebar content',
|
settingsTabsTitle: 'Sidebar content',
|
||||||
settingsViewersTitle: 'File viewers',
|
settingsViewersTitle: 'File viewers',
|
||||||
settingsGeneralTitle: 'General',
|
settingsGeneralTitle: 'General',
|
||||||
|
|||||||
+8
-1
@@ -10,12 +10,13 @@
|
|||||||
*/
|
*/
|
||||||
import type { api } from './api.ts'
|
import type { api } from './api.ts'
|
||||||
import {
|
import {
|
||||||
|
clampTerminalFontSize,
|
||||||
clampWidthPercent,
|
clampWidthPercent,
|
||||||
SIDEBAR_PREFS_DEFAULTS,
|
SIDEBAR_PREFS_DEFAULTS,
|
||||||
type SidebarPrefs,
|
type SidebarPrefs,
|
||||||
} from '../prefs-shared.ts'
|
} from '../prefs-shared.ts'
|
||||||
|
|
||||||
export { SIDEBAR_PREFS_DEFAULTS, clampWidthPercent }
|
export { SIDEBAR_PREFS_DEFAULTS, clampTerminalFontSize, clampWidthPercent }
|
||||||
export type { SidebarPrefs }
|
export type { SidebarPrefs }
|
||||||
|
|
||||||
/** The settings wire face the preferences need (a subset of the plugin api). */
|
/** The settings wire face the preferences need (a subset of the plugin api). */
|
||||||
@@ -49,6 +50,12 @@ export function parsePrefs(value: unknown): SidebarPrefs {
|
|||||||
bottomPanelAutoTerminal: typeof record.bottomPanelAutoTerminal === 'boolean'
|
bottomPanelAutoTerminal: typeof record.bottomPanelAutoTerminal === 'boolean'
|
||||||
? record.bottomPanelAutoTerminal
|
? record.bottomPanelAutoTerminal
|
||||||
: SIDEBAR_PREFS_DEFAULTS.bottomPanelAutoTerminal,
|
: SIDEBAR_PREFS_DEFAULTS.bottomPanelAutoTerminal,
|
||||||
|
terminalFontFamily: typeof record.terminalFontFamily === 'string'
|
||||||
|
? record.terminalFontFamily
|
||||||
|
: SIDEBAR_PREFS_DEFAULTS.terminalFontFamily,
|
||||||
|
terminalFontSize: typeof record.terminalFontSize === 'number' && Number.isFinite(record.terminalFontSize)
|
||||||
|
? clampTerminalFontSize(record.terminalFontSize)
|
||||||
|
: SIDEBAR_PREFS_DEFAULTS.terminalFontSize,
|
||||||
interceptOpenPath: typeof record.interceptOpenPath === 'boolean'
|
interceptOpenPath: typeof record.interceptOpenPath === 'boolean'
|
||||||
? record.interceptOpenPath
|
? record.interceptOpenPath
|
||||||
: SIDEBAR_PREFS_DEFAULTS.interceptOpenPath,
|
: SIDEBAR_PREFS_DEFAULTS.interceptOpenPath,
|
||||||
|
|||||||
+22
-5
@@ -28,9 +28,15 @@ import {
|
|||||||
import { isNarrowWidth } from './breakpoints.ts'
|
import { isNarrowWidth } from './breakpoints.ts'
|
||||||
import type { SessionScope } from './api.ts'
|
import type { SessionScope } from './api.ts'
|
||||||
|
|
||||||
/** One declarative boolean setting of a tab/viewer, rendered as a nested
|
/** The row control a declarative setting renders as in the settings popup. */
|
||||||
* switch row in the Side card settings page (e.g. the Subagent page's
|
export type SidebarSettingToggleType = 'switch' | 'text' | 'number'
|
||||||
* "auto-open when a subagent appears"). */
|
|
||||||
|
/** One declarative setting of a tab/viewer, rendered as a nested row in the
|
||||||
|
* Side card settings page (e.g. the Subagent page's "auto-open when a
|
||||||
|
* subagent appears" switch, or the terminal's custom font rows). `type`
|
||||||
|
* selects the control: 'switch' (default) renders the custom switch,
|
||||||
|
* 'text' a free-form input committed on blur/Enter, 'number' a numeric
|
||||||
|
* input clamped to `min`/`max`. */
|
||||||
export interface SidebarSettingToggle {
|
export interface SidebarSettingToggle {
|
||||||
/** The SidebarPrefs field this toggle reads and writes ('autoOpenSubagent'). */
|
/** The SidebarPrefs field this toggle reads and writes ('autoOpenSubagent'). */
|
||||||
key: string
|
key: string
|
||||||
@@ -38,15 +44,26 @@ export interface SidebarSettingToggle {
|
|||||||
title: string | (() => string)
|
title: string | (() => string)
|
||||||
/** Row description (i18n friendly). */
|
/** Row description (i18n friendly). */
|
||||||
desc?: string | (() => string)
|
desc?: string | (() => string)
|
||||||
|
/** Row control type; defaults to 'switch' (backward compatible). */
|
||||||
|
type?: SidebarSettingToggleType
|
||||||
|
/** Lower bound for `type: 'number'` rows (clamped on commit). */
|
||||||
|
min?: number
|
||||||
|
/** Upper bound for `type: 'number'` rows (clamped on commit). */
|
||||||
|
max?: number
|
||||||
|
/** Input placeholder for `type: 'text'` rows. */
|
||||||
|
placeholder?: string
|
||||||
|
/** Unit suffix rendered after the input (e.g. 'px' for a size row). */
|
||||||
|
unit?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Declarative settings of one registered tab or file viewer. */
|
/** Declarative settings of one registered tab or file viewer. */
|
||||||
export interface SidebarSettingsDeclaration {
|
export interface SidebarSettingsDeclaration {
|
||||||
/**
|
/**
|
||||||
* Extra boolean toggles rendered under the feature's own row in the
|
* Extra settings rows rendered under the feature's own row in the
|
||||||
* settings page (only while the feature is enabled). Keys must be fields
|
* settings page (only while the feature is enabled). Keys must be fields
|
||||||
* of the host's PrefsSchema (built-ins: 'autoOpenSubagent',
|
* of the host's PrefsSchema (built-ins: 'autoOpenSubagent',
|
||||||
* 'agentTerminalTools'); unknown keys are dropped by the settings seam.
|
* 'agentTerminalTools', 'terminalFontFamily'); unknown keys are dropped
|
||||||
|
* by the settings seam.
|
||||||
*/
|
*/
|
||||||
toggles?: readonly SidebarSettingToggle[]
|
toggles?: readonly SidebarSettingToggle[]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
/**
|
||||||
|
* Terminal font resolution: the user's custom font prefs (SidebarPrefs,
|
||||||
|
* configured under the terminal card's secondary settings) turned into the
|
||||||
|
* xterm options. Kept as a pure module (no DOM, no xterm) so the fallback
|
||||||
|
* chain and clamping are unit-testable without mounting a terminal.
|
||||||
|
*/
|
||||||
|
import { clampTerminalFontSize, type SidebarPrefs } from '../prefs-shared.ts'
|
||||||
|
|
||||||
|
/** The built-in fallback stack when neither the user nor the theme sets one. */
|
||||||
|
export const DEFAULT_TERMINAL_FONT_FAMILY = '"SF Mono", Menlo, Consolas, "Liberation Mono", monospace'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve the xterm font options for the given prefs.
|
||||||
|
* @param prefs - the current side card preferences.
|
||||||
|
* @param themeFontFamily - the app's theme code font (`--ds-font-family-code`
|
||||||
|
* token value, read live by the caller); undefined when the token is absent.
|
||||||
|
* @returns the `fontFamily` / `fontSize` xterm options.
|
||||||
|
*/
|
||||||
|
export function resolveTerminalFont(
|
||||||
|
prefs: SidebarPrefs,
|
||||||
|
themeFontFamily: string | undefined,
|
||||||
|
): { fontFamily: string; fontSize: number } {
|
||||||
|
const custom = prefs.terminalFontFamily.trim()
|
||||||
|
return {
|
||||||
|
fontFamily: custom !== '' ? custom : (themeFontFamily || DEFAULT_TERMINAL_FONT_FAMILY),
|
||||||
|
fontSize: clampTerminalFontSize(prefs.terminalFontSize),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,9 @@ import z from 'schemastery'
|
|||||||
import {
|
import {
|
||||||
SIDEBAR_PREFS_DEFAULTS,
|
SIDEBAR_PREFS_DEFAULTS,
|
||||||
SIDEBAR_PREFS_NS,
|
SIDEBAR_PREFS_NS,
|
||||||
|
TERMINAL_FONT_SIZE_DEFAULT,
|
||||||
|
TERMINAL_FONT_SIZE_MAX,
|
||||||
|
TERMINAL_FONT_SIZE_MIN,
|
||||||
WIDTH_PERCENT_DEFAULT,
|
WIDTH_PERCENT_DEFAULT,
|
||||||
WIDTH_PERCENT_MAX,
|
WIDTH_PERCENT_MAX,
|
||||||
WIDTH_PERCENT_MIN,
|
WIDTH_PERCENT_MIN,
|
||||||
@@ -18,6 +21,9 @@ import {
|
|||||||
export {
|
export {
|
||||||
SIDEBAR_PREFS_DEFAULTS,
|
SIDEBAR_PREFS_DEFAULTS,
|
||||||
SIDEBAR_PREFS_NS,
|
SIDEBAR_PREFS_NS,
|
||||||
|
TERMINAL_FONT_SIZE_DEFAULT,
|
||||||
|
TERMINAL_FONT_SIZE_MAX,
|
||||||
|
TERMINAL_FONT_SIZE_MIN,
|
||||||
WIDTH_PERCENT_DEFAULT,
|
WIDTH_PERCENT_DEFAULT,
|
||||||
WIDTH_PERCENT_MAX,
|
WIDTH_PERCENT_MAX,
|
||||||
WIDTH_PERCENT_MIN,
|
WIDTH_PERCENT_MIN,
|
||||||
@@ -82,6 +88,8 @@ export const PrefsSchema: z<SidebarPrefs> = z.object({
|
|||||||
autoOpenJobs: z.boolean().default(true),
|
autoOpenJobs: z.boolean().default(true),
|
||||||
agentTerminalTools: z.boolean().default(false),
|
agentTerminalTools: z.boolean().default(false),
|
||||||
bottomPanelAutoTerminal: z.boolean().default(true),
|
bottomPanelAutoTerminal: z.boolean().default(true),
|
||||||
|
terminalFontFamily: z.string().default(''),
|
||||||
|
terminalFontSize: z.number().step(1).min(TERMINAL_FONT_SIZE_MIN).max(TERMINAL_FONT_SIZE_MAX).default(TERMINAL_FONT_SIZE_DEFAULT),
|
||||||
interceptOpenPath: z.boolean().default(true),
|
interceptOpenPath: z.boolean().default(true),
|
||||||
htmlViewerNoSandbox: z.boolean().default(false),
|
htmlViewerNoSandbox: z.boolean().default(false),
|
||||||
htmlViewerDefaultUnsafe: z.boolean().default(false),
|
htmlViewerDefaultUnsafe: z.boolean().default(false),
|
||||||
|
|||||||
@@ -33,6 +33,18 @@ export interface SidebarPrefs {
|
|||||||
* user explicitly enables it in the side card settings.
|
* user explicitly enables it in the side card settings.
|
||||||
*/
|
*/
|
||||||
agentTerminalTools: boolean
|
agentTerminalTools: boolean
|
||||||
|
/**
|
||||||
|
* Custom terminal font-family stack (a CSS font-family value, e.g.
|
||||||
|
* `'JetBrains Mono', monospace`). Empty string follows the app's theme
|
||||||
|
* monospace font (`--ds-font-family-code`). Applied live to every
|
||||||
|
* terminal tab; configured under the terminal card's secondary settings.
|
||||||
|
*/
|
||||||
|
terminalFontFamily: string
|
||||||
|
/**
|
||||||
|
* Custom terminal font size in px (9–32). Applied live to every terminal
|
||||||
|
* tab; configured under the terminal card's secondary settings.
|
||||||
|
*/
|
||||||
|
terminalFontSize: number
|
||||||
/**
|
/**
|
||||||
* Whether expanding the bottom panel for the FIRST time in a session tries
|
* Whether expanding the bottom panel for the FIRST time in a session tries
|
||||||
* to open a fresh terminal tab there (the terminal quota/type still gates
|
* to open a fresh terminal tab there (the terminal quota/type still gates
|
||||||
@@ -104,6 +116,11 @@ export const WIDTH_PERCENT_MIN = 20
|
|||||||
export const WIDTH_PERCENT_MAX = 60
|
export const WIDTH_PERCENT_MAX = 60
|
||||||
export const WIDTH_PERCENT_DEFAULT = 30
|
export const WIDTH_PERCENT_DEFAULT = 30
|
||||||
|
|
||||||
|
/** Range contract of {@link SidebarPrefs.terminalFontSize}. */
|
||||||
|
export const TERMINAL_FONT_SIZE_MIN = 9
|
||||||
|
export const TERMINAL_FONT_SIZE_MAX = 32
|
||||||
|
export const TERMINAL_FONT_SIZE_DEFAULT = 13
|
||||||
|
|
||||||
/** Fallback prefs used whenever the settings document is unreachable or malformed. */
|
/** Fallback prefs used whenever the settings document is unreachable or malformed. */
|
||||||
export const SIDEBAR_PREFS_DEFAULTS: SidebarPrefs = {
|
export const SIDEBAR_PREFS_DEFAULTS: SidebarPrefs = {
|
||||||
openByDefault: true,
|
openByDefault: true,
|
||||||
@@ -112,6 +129,8 @@ export const SIDEBAR_PREFS_DEFAULTS: SidebarPrefs = {
|
|||||||
autoOpenJobs: true,
|
autoOpenJobs: true,
|
||||||
agentTerminalTools: false,
|
agentTerminalTools: false,
|
||||||
bottomPanelAutoTerminal: true,
|
bottomPanelAutoTerminal: true,
|
||||||
|
terminalFontFamily: '',
|
||||||
|
terminalFontSize: TERMINAL_FONT_SIZE_DEFAULT,
|
||||||
interceptOpenPath: true,
|
interceptOpenPath: true,
|
||||||
htmlViewerNoSandbox: false,
|
htmlViewerNoSandbox: false,
|
||||||
htmlViewerDefaultUnsafe: false,
|
htmlViewerDefaultUnsafe: false,
|
||||||
@@ -125,3 +144,8 @@ export const SIDEBAR_PREFS_DEFAULTS: SidebarPrefs = {
|
|||||||
export function clampWidthPercent(value: number): number {
|
export function clampWidthPercent(value: number): number {
|
||||||
return Math.min(WIDTH_PERCENT_MAX, Math.max(WIDTH_PERCENT_MIN, Math.round(value)))
|
return Math.min(WIDTH_PERCENT_MAX, Math.max(WIDTH_PERCENT_MIN, Math.round(value)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Clamp one terminal font size into the contract range (shared by schema and client reads). */
|
||||||
|
export function clampTerminalFontSize(value: number): number {
|
||||||
|
return Math.min(TERMINAL_FONT_SIZE_MAX, Math.max(TERMINAL_FONT_SIZE_MIN, Math.round(value)))
|
||||||
|
}
|
||||||
|
|||||||
+14
-2
@@ -47,10 +47,22 @@ describe('built-in tab registrations', () => {
|
|||||||
expect(toggles.map(t => t.key)).toEqual(['autoOpenSubagent', 'autoOpenJobs'])
|
expect(toggles.map(t => t.key)).toEqual(['autoOpenSubagent', 'autoOpenJobs'])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('the terminal tab declares the model terminal-tools related setting', () => {
|
it('the terminal tab declares the model terminal-tools, auto-terminal and custom-font settings', () => {
|
||||||
const { service } = setup()
|
const { service } = setup()
|
||||||
const toggles = service.getTab('terminal')?.settings?.toggles ?? []
|
const toggles = service.getTab('terminal')?.settings?.toggles ?? []
|
||||||
expect(toggles.map(t => t.key)).toEqual(['agentTerminalTools', 'bottomPanelAutoTerminal'])
|
expect(toggles.map(t => t.key)).toEqual(['agentTerminalTools', 'bottomPanelAutoTerminal', 'terminalFontFamily', 'terminalFontSize'])
|
||||||
|
// The font rows are text/number inputs (not switches), with the size
|
||||||
|
// row bounded by the shared 9–32 contract.
|
||||||
|
expect(toggles[2]?.type).toBe('text')
|
||||||
|
expect(toggles[2]?.title).toBeDefined()
|
||||||
|
expect(toggles[2]?.placeholder).toBeDefined()
|
||||||
|
expect(toggles[3]?.type).toBe('number')
|
||||||
|
expect(toggles[3]?.min).toBe(9)
|
||||||
|
expect(toggles[3]?.max).toBe(32)
|
||||||
|
expect(toggles[3]?.unit).toBe('px')
|
||||||
|
// The first two rows stay plain boolean switches.
|
||||||
|
expect(toggles[0]?.type ?? 'switch').toBe('switch')
|
||||||
|
expect(toggles[1]?.type ?? 'switch').toBe('switch')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('the browser tab declares its sandbox and link-takeover related settings', () => {
|
it('the browser tab declares its sandbox and link-takeover related settings', () => {
|
||||||
|
|||||||
@@ -50,6 +50,10 @@ describe('dsh-better-sidebar plugin export shape', () => {
|
|||||||
// The terminal tools default OFF (the feature is dormant until the user
|
// The terminal tools default OFF (the feature is dormant until the user
|
||||||
// enables it in the side card settings).
|
// enables it in the side card settings).
|
||||||
expect(resolved.agentTerminalTools).toBe(false)
|
expect(resolved.agentTerminalTools).toBe(false)
|
||||||
|
// The terminal font customizations default to the theme (empty family)
|
||||||
|
// and 13px.
|
||||||
|
expect(resolved.terminalFontFamily).toBe('')
|
||||||
|
expect(resolved.terminalFontSize).toBe(13)
|
||||||
// The enable-switch maps resolve to {} (everything on) for old documents.
|
// The enable-switch maps resolve to {} (everything on) for old documents.
|
||||||
expect(resolved.tabsEnabled).toEqual({})
|
expect(resolved.tabsEnabled).toEqual({})
|
||||||
expect(resolved.viewersEnabled).toEqual({})
|
expect(resolved.viewersEnabled).toEqual({})
|
||||||
@@ -59,6 +63,6 @@ describe('dsh-better-sidebar plugin export shape', () => {
|
|||||||
const overridden = (PrefsSchema as unknown as {
|
const overridden = (PrefsSchema as unknown as {
|
||||||
(input: Record<string, unknown> | undefined): Record<string, unknown>
|
(input: Record<string, unknown> | undefined): Record<string, unknown>
|
||||||
})({ openByDefault: false, defaultWidthPercent: 45 })
|
})({ openByDefault: false, defaultWidthPercent: 45 })
|
||||||
expect(overridden).toEqual({ openByDefault: false, defaultWidthPercent: 45, autoOpenSubagent: true, autoOpenJobs: true, agentTerminalTools: false, bottomPanelAutoTerminal: true, interceptOpenPath: true, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, tabsEnabled: {}, viewersEnabled: {} })
|
expect(overridden).toEqual({ openByDefault: false, defaultWidthPercent: 45, autoOpenSubagent: true, autoOpenJobs: true, agentTerminalTools: false, bottomPanelAutoTerminal: true, terminalFontFamily: '', terminalFontSize: 13, interceptOpenPath: true, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, tabsEnabled: {}, viewersEnabled: {} })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,175 @@
|
|||||||
|
/**
|
||||||
|
* Interactive tests for the settings popup's text/number rows: the draft is
|
||||||
|
* local state committed on blur through the parent's onCommit; the parent's
|
||||||
|
* canonical return is adopted (clamped numbers, stored value for invalid
|
||||||
|
* input), and the row remounts when the committed pref value changes (a
|
||||||
|
* failed commit reverts prefs → the stored value reappears).
|
||||||
|
*
|
||||||
|
* Rendered with createRoot + act() in jsdom (the SSR specs stay in
|
||||||
|
* side-card-section.spec.tsx; this file exercises the event paths).
|
||||||
|
*/
|
||||||
|
// @vitest-environment jsdom
|
||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { createElement, type ReactNode } from 'react'
|
||||||
|
import { createRoot, type Root } from 'react-dom/client'
|
||||||
|
import { act } from 'react-dom/test-utils'
|
||||||
|
|
||||||
|
// The act() environment flag (React 18.2 reads it before flushing effects).
|
||||||
|
;(globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true
|
||||||
|
import type { SidebarSettingToggle } from '../src/client/service.ts'
|
||||||
|
import { FeatureSettingsRows } from '../src/client/SideCardSection.tsx'
|
||||||
|
import { SIDEBAR_PREFS_DEFAULTS } from '../src/prefs-shared.ts'
|
||||||
|
|
||||||
|
/** Render the rows into a detached container under React's act(). */
|
||||||
|
function mount(node: ReactNode): { container: HTMLDivElement; rerender: (node: ReactNode) => void; unmount: () => void } {
|
||||||
|
const container = document.createElement('div')
|
||||||
|
document.body.append(container)
|
||||||
|
const root: Root = createRoot(container)
|
||||||
|
act(() => { root.render(node) })
|
||||||
|
return {
|
||||||
|
container,
|
||||||
|
rerender: (next) => { act(() => { root.render(next) }) },
|
||||||
|
unmount: () => {
|
||||||
|
act(() => { root.unmount() })
|
||||||
|
container.remove()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Type into an input and commit it via blur (React 18: input event +
|
||||||
|
* focusout). The native setter bypasses React's value tracker so the
|
||||||
|
* change is actually seen. */
|
||||||
|
function typeAndBlur(input: HTMLInputElement, value: string): void {
|
||||||
|
const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')!.set!
|
||||||
|
act(() => {
|
||||||
|
setter.call(input, value)
|
||||||
|
input.dispatchEvent(new Event('input', { bubbles: true }))
|
||||||
|
input.dispatchEvent(new FocusEvent('focusout', { bubbles: true }))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const prefs = { ...SIDEBAR_PREFS_DEFAULTS }
|
||||||
|
|
||||||
|
describe('FeatureSettingsRows typed rows (interactive)', () => {
|
||||||
|
it('commits the raw text on blur and adopts the canonical return', () => {
|
||||||
|
const commits: Array<[string, string]> = []
|
||||||
|
const toggle: SidebarSettingToggle = {
|
||||||
|
key: 'terminalFontFamily',
|
||||||
|
type: 'text',
|
||||||
|
title: () => 'Font family',
|
||||||
|
}
|
||||||
|
const { container, unmount } = mount(createElement(FeatureSettingsRows, {
|
||||||
|
toggles: [toggle],
|
||||||
|
prefs,
|
||||||
|
onToggle: () => {},
|
||||||
|
onCommit: (t, raw) => {
|
||||||
|
commits.push([t.key, raw])
|
||||||
|
return raw
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
const input = container.querySelector('input')!
|
||||||
|
typeAndBlur(input, 'Monaco')
|
||||||
|
expect(commits).toEqual([['terminalFontFamily', 'Monaco']])
|
||||||
|
// The canonical return is adopted into the draft.
|
||||||
|
expect(input.value).toBe('Monaco')
|
||||||
|
unmount()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('clamps numbers into the declared bounds on commit', () => {
|
||||||
|
const commits: Array<[string, number]> = []
|
||||||
|
const toggle: SidebarSettingToggle = {
|
||||||
|
key: 'terminalFontSize',
|
||||||
|
type: 'number',
|
||||||
|
title: () => 'Font size',
|
||||||
|
min: 9,
|
||||||
|
max: 32,
|
||||||
|
}
|
||||||
|
const { container, unmount } = mount(createElement(FeatureSettingsRows, {
|
||||||
|
toggles: [toggle],
|
||||||
|
prefs: { ...prefs, terminalFontSize: 13 },
|
||||||
|
onToggle: () => {},
|
||||||
|
onCommit: (t, raw) => {
|
||||||
|
const parsed = Number(raw)
|
||||||
|
const clamped = Math.min(32, Math.max(9, Math.round(parsed)))
|
||||||
|
commits.push([t.key, clamped])
|
||||||
|
return String(clamped)
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
const input = container.querySelector('input')!
|
||||||
|
typeAndBlur(input, '40')
|
||||||
|
expect(commits).toEqual([['terminalFontSize', 32]])
|
||||||
|
expect(input.value).toBe('32')
|
||||||
|
unmount()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('clamps an emptied number input to the lower bound on commit (width-row precedent)', () => {
|
||||||
|
const commits: Array<[string, number]> = []
|
||||||
|
const toggle: SidebarSettingToggle = {
|
||||||
|
key: 'terminalFontSize',
|
||||||
|
type: 'number',
|
||||||
|
title: () => 'Font size',
|
||||||
|
min: 9,
|
||||||
|
max: 32,
|
||||||
|
}
|
||||||
|
const { container, unmount } = mount(createElement(FeatureSettingsRows, {
|
||||||
|
toggles: [toggle],
|
||||||
|
prefs: { ...prefs, terminalFontSize: 13 },
|
||||||
|
onToggle: () => {},
|
||||||
|
// The parent mirrors the real handler: an emptied number parses to 0
|
||||||
|
// and clamps into the bounds (a browser number input never holds a
|
||||||
|
// non-numeric string — the draft can only be empty or numeric).
|
||||||
|
onCommit: (t, raw) => {
|
||||||
|
const clamped = Math.min(32, Math.max(9, Math.round(Number(raw))))
|
||||||
|
commits.push([t.key, clamped])
|
||||||
|
return String(clamped)
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
const input = container.querySelector('input')!
|
||||||
|
typeAndBlur(input, '')
|
||||||
|
expect(commits).toEqual([['terminalFontSize', 9]])
|
||||||
|
expect(input.value).toBe('9')
|
||||||
|
unmount()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('reverts the draft to the committed value after a failed commit reverts prefs', () => {
|
||||||
|
const toggle: SidebarSettingToggle = {
|
||||||
|
key: 'terminalFontFamily',
|
||||||
|
type: 'text',
|
||||||
|
title: () => 'Font family',
|
||||||
|
}
|
||||||
|
// The parent mirrors the real handler: the optimistic commit adopts the
|
||||||
|
// typed value, and a FAILED write reverts prefs to the stored one.
|
||||||
|
let prefsNow = { ...prefs, terminalFontFamily: 'Old' }
|
||||||
|
const { container, rerender, unmount } = mount(createElement(FeatureSettingsRows, {
|
||||||
|
toggles: [toggle],
|
||||||
|
prefs: prefsNow,
|
||||||
|
onToggle: () => {},
|
||||||
|
onCommit: (_t, raw) => raw,
|
||||||
|
}))
|
||||||
|
const input = container.querySelector('input')!
|
||||||
|
typeAndBlur(input, 'New')
|
||||||
|
// Optimistic commit: the draft adopts the typed value.
|
||||||
|
expect(input.value).toBe('New')
|
||||||
|
// The optimistic pref lands (parent state): the key changes, the row
|
||||||
|
// remounts with the same value — no visible reset while editing.
|
||||||
|
prefsNow = { ...prefsNow, terminalFontFamily: 'New' }
|
||||||
|
rerender(createElement(FeatureSettingsRows, {
|
||||||
|
toggles: [toggle],
|
||||||
|
prefs: prefsNow,
|
||||||
|
onToggle: () => {},
|
||||||
|
onCommit: (_t, raw) => raw,
|
||||||
|
}))
|
||||||
|
expect(container.querySelector('input')!.value).toBe('New')
|
||||||
|
// The write fails: prefs revert to the stored value and the row
|
||||||
|
// remounts with it (the stale draft is gone).
|
||||||
|
prefsNow = { ...prefsNow, terminalFontFamily: 'Old' }
|
||||||
|
rerender(createElement(FeatureSettingsRows, {
|
||||||
|
toggles: [toggle],
|
||||||
|
prefs: prefsNow,
|
||||||
|
onToggle: () => {},
|
||||||
|
onCommit: (_t, raw) => raw,
|
||||||
|
}))
|
||||||
|
expect(container.querySelector('input')!.value).toBe('Old')
|
||||||
|
unmount()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -172,4 +172,47 @@ describe('FeatureSettingsRows (the secondary settings popup body)', () => {
|
|||||||
}))
|
}))
|
||||||
expect(html).toContain('checked=""')
|
expect(html).toContain('checked=""')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('renders a text row as an input seeded with the pref value (empty = theme default)', () => {
|
||||||
|
const html = renderToString(createElement(FeatureSettingsRows, {
|
||||||
|
toggles: [{
|
||||||
|
key: 'terminalFontFamily',
|
||||||
|
type: 'text',
|
||||||
|
title: () => 'Font family',
|
||||||
|
desc: () => 'CSS stack',
|
||||||
|
placeholder: '"JetBrains Mono", monospace',
|
||||||
|
}],
|
||||||
|
prefs: { ...prefs, terminalFontFamily: '"JetBrains Mono", monospace' },
|
||||||
|
onToggle: () => {},
|
||||||
|
onCommit: () => '',
|
||||||
|
}))
|
||||||
|
expect(html).toContain('Font family')
|
||||||
|
expect(html).toContain('placeholder=""JetBrains Mono", monospace"')
|
||||||
|
// The input carries the pref value (no switch for text rows).
|
||||||
|
expect(html).toContain('value=""JetBrains Mono", monospace"')
|
||||||
|
expect(html).not.toContain('type="checkbox"')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders a number row with the pref value, the declared bounds and a unit suffix', () => {
|
||||||
|
const html = renderToString(createElement(FeatureSettingsRows, {
|
||||||
|
toggles: [{
|
||||||
|
key: 'terminalFontSize',
|
||||||
|
type: 'number',
|
||||||
|
title: () => 'Font size',
|
||||||
|
min: 9,
|
||||||
|
max: 32,
|
||||||
|
unit: 'px',
|
||||||
|
}],
|
||||||
|
prefs: { ...prefs, terminalFontSize: 18 },
|
||||||
|
onToggle: () => {},
|
||||||
|
onCommit: () => '18',
|
||||||
|
}))
|
||||||
|
expect(html).toContain('Font size')
|
||||||
|
expect(html).toContain('type="number"')
|
||||||
|
expect(html).toContain('value="18"')
|
||||||
|
expect(html).toContain('min="9"')
|
||||||
|
expect(html).toContain('max="32"')
|
||||||
|
expect(html).toContain('px')
|
||||||
|
expect(html).not.toContain('type="checkbox"')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -517,6 +517,8 @@ describe('side card settings routes', () => {
|
|||||||
autoOpenJobs: true,
|
autoOpenJobs: true,
|
||||||
agentTerminalTools: false,
|
agentTerminalTools: false,
|
||||||
bottomPanelAutoTerminal: true,
|
bottomPanelAutoTerminal: true,
|
||||||
|
terminalFontFamily: '',
|
||||||
|
terminalFontSize: 13,
|
||||||
interceptOpenPath: true,
|
interceptOpenPath: true,
|
||||||
htmlViewerNoSandbox: false,
|
htmlViewerNoSandbox: false,
|
||||||
htmlViewerDefaultUnsafe: false,
|
htmlViewerDefaultUnsafe: false,
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* Terminal font resolution tests: the custom font prefs (side card settings,
|
||||||
|
* terminal card) resolve into xterm options with a strict fallback chain —
|
||||||
|
* user family > theme code font > the built-in monospace stack, and the size
|
||||||
|
* clamped into the 9–32 contract.
|
||||||
|
*/
|
||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { SIDEBAR_PREFS_DEFAULTS, clampTerminalFontSize, TERMINAL_FONT_SIZE_MAX, TERMINAL_FONT_SIZE_MIN } from '../src/prefs-shared.ts'
|
||||||
|
import { DEFAULT_TERMINAL_FONT_FAMILY, resolveTerminalFont } from '../src/client/terminal-font.ts'
|
||||||
|
|
||||||
|
describe('clampTerminalFontSize', () => {
|
||||||
|
it('rounds and clamps into the 9–32 contract', () => {
|
||||||
|
expect(clampTerminalFontSize(13)).toBe(13)
|
||||||
|
expect(clampTerminalFontSize(15.6)).toBe(16)
|
||||||
|
expect(clampTerminalFontSize(5)).toBe(TERMINAL_FONT_SIZE_MIN)
|
||||||
|
expect(clampTerminalFontSize(40)).toBe(TERMINAL_FONT_SIZE_MAX)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('resolveTerminalFont', () => {
|
||||||
|
it('falls back to the theme code font when the family pref is empty', () => {
|
||||||
|
const { fontFamily, fontSize } = resolveTerminalFont(SIDEBAR_PREFS_DEFAULTS, 'var-theme-font')
|
||||||
|
expect(fontFamily).toBe('var-theme-font')
|
||||||
|
expect(fontSize).toBe(13)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('falls back to the built-in monospace stack when neither the pref nor the theme provides one', () => {
|
||||||
|
expect(resolveTerminalFont(SIDEBAR_PREFS_DEFAULTS, undefined).fontFamily).toBe(DEFAULT_TERMINAL_FONT_FAMILY)
|
||||||
|
// A whitespace-only pref counts as empty (theme default).
|
||||||
|
expect(resolveTerminalFont({ ...SIDEBAR_PREFS_DEFAULTS, terminalFontFamily: ' ' }, undefined).fontFamily)
|
||||||
|
.toBe(DEFAULT_TERMINAL_FONT_FAMILY)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('prefers the custom family verbatim and clamps the size', () => {
|
||||||
|
const { fontFamily, fontSize } = resolveTerminalFont(
|
||||||
|
{ ...SIDEBAR_PREFS_DEFAULTS, terminalFontFamily: '"JetBrains Mono", monospace', terminalFontSize: 40 },
|
||||||
|
'var-theme-font',
|
||||||
|
)
|
||||||
|
expect(fontFamily).toBe('"JetBrains Mono", monospace')
|
||||||
|
expect(fontSize).toBe(TERMINAL_FONT_SIZE_MAX)
|
||||||
|
})
|
||||||
|
})
|
||||||
+26
-4
@@ -1153,6 +1153,8 @@ describe('side card preferences', () => {
|
|||||||
autoOpenJobs: true,
|
autoOpenJobs: true,
|
||||||
agentTerminalTools: true,
|
agentTerminalTools: true,
|
||||||
bottomPanelAutoTerminal: true,
|
bottomPanelAutoTerminal: true,
|
||||||
|
terminalFontFamily: '',
|
||||||
|
terminalFontSize: 13,
|
||||||
interceptOpenPath: true,
|
interceptOpenPath: true,
|
||||||
htmlViewerNoSandbox: false,
|
htmlViewerNoSandbox: false,
|
||||||
htmlViewerDefaultUnsafe: false,
|
htmlViewerDefaultUnsafe: false,
|
||||||
@@ -1172,6 +1174,8 @@ describe('side card preferences', () => {
|
|||||||
autoOpenJobs: true,
|
autoOpenJobs: true,
|
||||||
agentTerminalTools: false,
|
agentTerminalTools: false,
|
||||||
bottomPanelAutoTerminal: true,
|
bottomPanelAutoTerminal: true,
|
||||||
|
terminalFontFamily: '',
|
||||||
|
terminalFontSize: 13,
|
||||||
interceptOpenPath: true,
|
interceptOpenPath: true,
|
||||||
htmlViewerNoSandbox: false,
|
htmlViewerNoSandbox: false,
|
||||||
htmlViewerDefaultUnsafe: false,
|
htmlViewerDefaultUnsafe: false,
|
||||||
@@ -1191,6 +1195,8 @@ describe('side card preferences', () => {
|
|||||||
autoOpenJobs: true,
|
autoOpenJobs: true,
|
||||||
agentTerminalTools: false,
|
agentTerminalTools: false,
|
||||||
bottomPanelAutoTerminal: true,
|
bottomPanelAutoTerminal: true,
|
||||||
|
terminalFontFamily: '',
|
||||||
|
terminalFontSize: 13,
|
||||||
interceptOpenPath: true,
|
interceptOpenPath: true,
|
||||||
htmlViewerNoSandbox: false,
|
htmlViewerNoSandbox: false,
|
||||||
htmlViewerDefaultUnsafe: false,
|
htmlViewerDefaultUnsafe: false,
|
||||||
@@ -1225,6 +1231,22 @@ describe('side card preferences', () => {
|
|||||||
expect((await loadPrefs(wire({ interceptOpenPath: true }))).interceptOpenPath).toBe(true)
|
expect((await loadPrefs(wire({ interceptOpenPath: true }))).interceptOpenPath).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('resolves the terminal font prefs (family passthrough, size clamp)', async () => {
|
||||||
|
// Absent → theme default (empty family) + default size.
|
||||||
|
expect((await loadPrefs(wire({}))).terminalFontFamily).toBe('')
|
||||||
|
expect((await loadPrefs(wire({}))).terminalFontSize).toBe(13)
|
||||||
|
// A custom family survives verbatim; a malformed one falls back.
|
||||||
|
expect((await loadPrefs(wire({ terminalFontFamily: '"JetBrains Mono", monospace' }))).terminalFontFamily)
|
||||||
|
.toBe('"JetBrains Mono", monospace')
|
||||||
|
expect((await loadPrefs(wire({ terminalFontFamily: 42 }))).terminalFontFamily).toBe('')
|
||||||
|
// The size clamps into 9–32 (rounded); non-numbers fall back.
|
||||||
|
expect((await loadPrefs(wire({ terminalFontSize: 5 }))).terminalFontSize).toBe(9)
|
||||||
|
expect((await loadPrefs(wire({ terminalFontSize: 40 }))).terminalFontSize).toBe(32)
|
||||||
|
expect((await loadPrefs(wire({ terminalFontSize: 15.6 }))).terminalFontSize).toBe(16)
|
||||||
|
expect((await loadPrefs(wire({ terminalFontSize: 'big' }))).terminalFontSize).toBe(13)
|
||||||
|
expect((await loadPrefs(wire({ terminalFontSize: 18 }))).terminalFontSize).toBe(18)
|
||||||
|
})
|
||||||
|
|
||||||
it('validates the per-tab / per-viewer enable maps (absent keys mean enabled)', async () => {
|
it('validates the per-tab / per-viewer enable maps (absent keys mean enabled)', async () => {
|
||||||
// A non-object map falls back to {} (everything enabled).
|
// A non-object map falls back to {} (everything enabled).
|
||||||
expect((await loadPrefs(wire({ tabsEnabled: 'nope' }))).tabsEnabled).toEqual({})
|
expect((await loadPrefs(wire({ tabsEnabled: 'nope' }))).tabsEnabled).toEqual({})
|
||||||
@@ -1242,9 +1264,9 @@ describe('side card preferences', () => {
|
|||||||
const store = createSidebarStore()
|
const store = createSidebarStore()
|
||||||
// Node environment: no window → the width falls back to PANEL_DEFAULT,
|
// Node environment: no window → the width falls back to PANEL_DEFAULT,
|
||||||
// while the open flag still follows the preference.
|
// while the open flag still follows the preference.
|
||||||
store.setPrefs({ openByDefault: false, defaultWidthPercent: 45, autoOpenSubagent: true, autoOpenJobs: true, agentTerminalTools: false, bottomPanelAutoTerminal: true, interceptOpenPath: true, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, tabsEnabled: {}, viewersEnabled: {} })
|
store.setPrefs({ openByDefault: false, defaultWidthPercent: 45, autoOpenSubagent: true, autoOpenJobs: true, agentTerminalTools: false, bottomPanelAutoTerminal: true, terminalFontFamily: '', terminalFontSize: 13, interceptOpenPath: true, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, tabsEnabled: {}, viewersEnabled: {} })
|
||||||
store.setSession('fresh-session')
|
store.setSession('fresh-session')
|
||||||
expect(store.getPrefs()).toEqual({ openByDefault: false, defaultWidthPercent: 45, autoOpenSubagent: true, autoOpenJobs: true, agentTerminalTools: false, bottomPanelAutoTerminal: true, interceptOpenPath: true, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, tabsEnabled: {}, viewersEnabled: {} })
|
expect(store.getPrefs()).toEqual({ openByDefault: false, defaultWidthPercent: 45, autoOpenSubagent: true, autoOpenJobs: true, agentTerminalTools: false, bottomPanelAutoTerminal: true, terminalFontFamily: '', terminalFontSize: 13, interceptOpenPath: true, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, tabsEnabled: {}, viewersEnabled: {} })
|
||||||
const snapshot = store.getSnapshot()
|
const snapshot = store.getSnapshot()
|
||||||
expect(snapshot.sessionId).toBe('fresh-session')
|
expect(snapshot.sessionId).toBe('fresh-session')
|
||||||
expect(snapshot.state?.panelOpen).toBe(false)
|
expect(snapshot.state?.panelOpen).toBe(false)
|
||||||
@@ -1280,7 +1302,7 @@ describe('side card preferences', () => {
|
|||||||
|
|
||||||
it('skips the default explorer tab when the explorer type is disabled', () => {
|
it('skips the default explorer tab when the explorer type is disabled', () => {
|
||||||
const store = createSidebarStore()
|
const store = createSidebarStore()
|
||||||
store.setPrefs({ openByDefault: true, defaultWidthPercent: 30, autoOpenSubagent: true, autoOpenJobs: true, agentTerminalTools: false, bottomPanelAutoTerminal: true, interceptOpenPath: true, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, tabsEnabled: { explorer: false }, viewersEnabled: {} })
|
store.setPrefs({ openByDefault: true, defaultWidthPercent: 30, autoOpenSubagent: true, autoOpenJobs: true, agentTerminalTools: false, bottomPanelAutoTerminal: true, terminalFontFamily: '', terminalFontSize: 13, interceptOpenPath: true, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, tabsEnabled: { explorer: false }, viewersEnabled: {} })
|
||||||
store.setSession('no-explorer')
|
store.setSession('no-explorer')
|
||||||
const state = store.getSnapshot().state!
|
const state = store.getSnapshot().state!
|
||||||
const tabs = allLeaves(state.splits).flatMap(leaf => leaf.tabs)
|
const tabs = allLeaves(state.splits).flatMap(leaf => leaf.tabs)
|
||||||
@@ -1288,7 +1310,7 @@ describe('side card preferences', () => {
|
|||||||
expect(state.splits.kind).toBe('leaf')
|
expect(state.splits.kind).toBe('leaf')
|
||||||
// Re-enabling seeds the explorer tab again.
|
// Re-enabling seeds the explorer tab again.
|
||||||
const openStore = createSidebarStore()
|
const openStore = createSidebarStore()
|
||||||
openStore.setPrefs({ openByDefault: true, defaultWidthPercent: 30, autoOpenSubagent: true, autoOpenJobs: true, agentTerminalTools: false, bottomPanelAutoTerminal: true, interceptOpenPath: true, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, tabsEnabled: {}, viewersEnabled: {} })
|
openStore.setPrefs({ openByDefault: true, defaultWidthPercent: 30, autoOpenSubagent: true, autoOpenJobs: true, agentTerminalTools: false, bottomPanelAutoTerminal: true, terminalFontFamily: '', terminalFontSize: 13, interceptOpenPath: true, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, tabsEnabled: {}, viewersEnabled: {} })
|
||||||
openStore.setSession('with-explorer')
|
openStore.setSession('with-explorer')
|
||||||
const openTabs = allLeaves(openStore.getSnapshot().state!.splits).flatMap(leaf => leaf.tabs)
|
const openTabs = allLeaves(openStore.getSnapshot().state!.splits).flatMap(leaf => leaf.tabs)
|
||||||
expect(openTabs.map(tab => tab.type)).toEqual(['explorer'])
|
expect(openTabs.map(tab => tab.type)).toEqual(['explorer'])
|
||||||
|
|||||||
Reference in New Issue
Block a user