/** * 工作台外观:4 套配色(skin)× 明暗模式(mode)× 终端渲染风格(termStyle)。 * * 三件事永远一起读、一起写,所以共用一条 localStorage 记录 * (`botmux.agent-workbench.appearance.v1`),命名沿用工作台既有的 * `botmux.agent-workbench.<域>.v` 家族(见 agent-workbench-storage.ts)。 * * 与全站明暗机制的关系(preferences.ts / ui.ts): * - `mode` 与全站 `ThemeMode` 同形(system / light / dark),决定**明暗族**; * - `skin` 决定**深色族里的哪一套**——浅色族只有 `light-frost` 一个候选, * 所以色板上选的那一枚只是「夜里用哪套」,跟随系统时在它与 light-frost 之间切; * - 首次进工作台没有本地记录时,`mode` 继承全站 `readStoredThemeMode()` 的值, * 之后工作台存自己的一份,全站再改浅色/深色不覆盖工作台的显式选择。 */ import { type ResolvedTheme, type ThemeMode } from './preferences.js'; import type { WorkbenchStorage } from './agent-workbench-storage.js'; export type WorkbenchSkinId = 'ink' | 'slate-blue' | 'warm-graphite' | 'light-frost'; /** 终端画布的渲染取向:`reader` 低饱和大行距、跟随工作台配色,`classic` 保持终端本色 * (现状渲染)。首版这两个值叫 `orca` / `orca-ink`,见下面的 LEGACY_* 迁移表。 */ export type WorkbenchTermStyle = 'reader' | 'classic'; export interface WorkbenchAppearance { skin: WorkbenchSkinId; mode: ThemeMode; termStyle: WorkbenchTermStyle; } export declare const WORKBENCH_APPEARANCE_STORAGE_KEY = "botmux.agent-workbench.appearance.v1"; export declare const WORKBENCH_SKIN_IDS: readonly WorkbenchSkinId[]; export declare const WORKBENCH_TERM_STYLES: readonly WorkbenchTermStyle[]; /** 认新值,也认旧值;都不认返回 null。类型守卫本身保持严格,只认新值。 */ export declare function migrateWorkbenchSkin(value: unknown): WorkbenchSkinId | null; export declare function migrateWorkbenchTermStyle(value: unknown): WorkbenchTermStyle | null; /** 浅色族唯一的一套:`mode` 落到 light 时恒用它,不占色板的「深色代表」名额。 */ export declare const WORKBENCH_LIGHT_SKIN: WorkbenchSkinId; /** 默认深色代表:与现有品牌观感最接近的一套,迁移成本最低。 */ export declare const DEFAULT_WORKBENCH_SKIN: WorkbenchSkinId; /** 默认终端渲染:`classic` 即现状渲染,存量用户零变化。 */ export declare const DEFAULT_WORKBENCH_TERM_STYLE: WorkbenchTermStyle; export declare function isWorkbenchSkin(value: unknown): value is WorkbenchSkinId; export declare function isWorkbenchTermStyle(value: unknown): value is WorkbenchTermStyle; /** 皮肤所属明暗族。`light-frost` 是浅色,其余三套都是深色。 */ export declare function workbenchSkinFamily(skin: WorkbenchSkinId): ResolvedTheme; export declare function defaultWorkbenchAppearance(inheritedMode?: ThemeMode): WorkbenchAppearance; /** 未知 skin / mode / termStyle 一律逐字段回落默认,绝不整份丢弃、更不抛错—— * 手改过 localStorage 或旧版本残留只该丢掉那一个字段,不该让工作台白屏。 * 首版命名(`orca` / `orca-ink`)在这里被认成新值,不算「未知」。 * 这是**唯一**的入口:loadWorkbenchAppearance、saveWorkbenchAppearance 与跨 tab 的 * storage 事件全部经过它,所以三条路径的迁移行为天然一致。 */ export declare function normalizeWorkbenchAppearance(value: unknown, inheritedMode?: ThemeMode): WorkbenchAppearance; export declare function loadWorkbenchAppearance(storage: WorkbenchStorage | null | undefined, inheritedMode?: ThemeMode): WorkbenchAppearance; export declare function saveWorkbenchAppearance(storage: WorkbenchStorage | null | undefined, appearance: WorkbenchAppearance): boolean; /** 实际生效的那一套配色:先由 mode(+ 系统偏好)定明暗族,再在族内取色。 */ export declare function resolveWorkbenchSkin(appearance: WorkbenchAppearance, systemPrefersDark: boolean): WorkbenchSkinId; /** * 点色板。三条规则来自设计规范「五 5.2」,不写会出「点了没反应」的投诉: * 1. 跟随系统 / 深色下点浅色那套 → 视为「我要一直浅色」,mode 切到 light; * 2. 跟随系统下点任一深色 → 只换深色代表,mode 保持 system(说的是「夜里用这套」); * 3. 浅色下点任一深色 → mode 切到 dark(规则 1 的镜像)。 * 选浅色时 `skin` 保持原来的深色代表不动 —— 再切回深色/跟随系统时还是用户上次选的那套。 */ export declare function selectWorkbenchSkin(current: WorkbenchAppearance, skin: WorkbenchSkinId): WorkbenchAppearance; export declare function selectWorkbenchMode(current: WorkbenchAppearance, mode: ThemeMode): WorkbenchAppearance; export declare function selectWorkbenchTermStyle(current: WorkbenchAppearance, termStyle: WorkbenchTermStyle): WorkbenchAppearance; /** 终端容器 class:几何(行距 / 内边距)差异全部由样式侧的这两个类承担。 */ export declare function workbenchTermContainerClass(termStyle: WorkbenchTermStyle): string; export interface WorkbenchSkinPreview { bg: string; raise: string; accent: string; } export declare const WORKBENCH_SKIN_PREVIEWS: Record; /** xterm.js `ITheme` 的子集:只给两套预设真正用到的键,避免把 xterm 类型拖进来。 */ export interface WorkbenchTermTheme { background: string; foreground: string; cursor: string; selectionBackground: string; black: string; red: string; green: string; yellow: string; blue: string; magenta: string; cyan: string; white: string; } /** * `classic` = 终端页现有配色**原样**(Tokyo Night,src/worker.ts 里那份字面量)。 * 存量用户切不切主题都看不出变化,这也是默认风格。 */ export declare const WORKBENCH_CLASSIC_TERM_THEME: WorkbenchTermTheme; /** * `reader`(「阅读」)= 低饱和 muted 版:直接映射该套配色的 token(--term-bg / * --text-1 / --text-3 / --accent / --ok / --warn / --err),所以终端和工作台是同一套色。 * `light-frost` 的终端画布仍是深色(彩色 ANSI 放浅底上不可读,业界通行做法), * 所以它这一行用的是规范里为它单列的那组亮色终端令牌。 */ export declare const WORKBENCH_READER_TERM_THEMES: Record; export declare function workbenchTermTheme(termStyle: WorkbenchTermStyle, skin: WorkbenchSkinId): WorkbenchTermTheme; /** * xterm 的 `lineHeight`。这是**唯一**的出处:终端页那段监听器(src/worker.ts 的模板 * 字符串,没法 import)必须照抄同样的数字,接缝测试按这张表比对它的字面量; * `.wb-term-classic / .wb-term-reader` 的 `--term-line-height` 同样按这张表对齐。 * * 阅读风的行距一路在收:1.55 → 1.3 → 1.15。1.55 下单元格高 ≈24.5px,CLI 的底部 chrome * (提示 + 输入框 + 状态条,固定占 8 个终端行)就要吃掉约 196px —— 900 高的窗口里四分 * 之一屏全是 chrome,所以先降到 1.3(≈20.5px),chrome 收回约 16%、可视行数多出两成。 * 1.3 对英文正文够用,可中文密集的 TUI 内容仍偏松:汉字字形本来就撑满 em 框,1.3 留出 * 的行间空隙观感已经接近隔行,一段中文反而更难顺着往下读(用户连续反馈过)。于是再收 * 一档到 1.15(≈18.2px):比经典的 1.0 还松一成半,中英文都留着呼吸感,行与行不再散开。 * `classic` 恒为 1 = xterm 默认,「经典 = 原样」的一部分。 */ export declare const WORKBENCH_TERM_LINE_HEIGHTS: Record; export declare function workbenchTermLineHeight(termStyle: WorkbenchTermStyle): number; /** 终端外壳(安全边距那圈)要刷的底色变量名。见 `workbenchTermCanvasStyle`。 */ export declare const WORKBENCH_TERM_CANVAS_BG_VAR = "--term-canvas-bg"; /** * 终端面板外壳的底色 —— 跟着**实际生效的终端渲染风格**走,而不是跟着皮肤令牌走。 * * 外壳(`.wb-pane-frame-shell`)给字形留了 8px 安全边距,这圈底色原本刷的是皮肤的 * `--term-bg`。阅读风时两者本来就同色(`WORKBENCH_READER_TERM_THEMES[skin].background` * 逐套等于该皮肤的 `--term-bg`),可经典渲染的 xterm 底色是它自己的 Tokyo Night * `#1a1b26`,和 `--term-bg`(如 slate-blue 的 `#030407`)差一大截,画布外就露出一圈 * 更黑的边框 —— 用户看到的「经典终端黑色边框」就是它。 * * 取值直接来自 `workbenchTermTheme()`,也就是同一次下发给终端 iframe 的那份 theme: * 外壳和画布永远同源同色,不存在第二份字面量。 */ export declare function workbenchTermCanvasStyle(termStyle: WorkbenchTermStyle, skin: WorkbenchSkinId): Record; /** 父页 → 终端 iframe 的一次外观下发。终端页只认这一个消息类型。 */ export declare const WORKBENCH_TERM_APPEARANCE_MESSAGE = "botmux:wb-appearance"; export interface WorkbenchTermAppearanceMessage { type: typeof WORKBENCH_TERM_APPEARANCE_MESSAGE; termStyle: WorkbenchTermStyle; skin: WorkbenchSkinId; theme: WorkbenchTermTheme; } export declare function workbenchTermAppearanceMessage(termStyle: WorkbenchTermStyle, skin: WorkbenchSkinId): WorkbenchTermAppearanceMessage; /** * 把外观推给终端 iframe。终端画布住在跨文档的 `/s/` 里,父页换 class * 它收不到,不推过去就会出现「工作台换了配色、终端还是旧色」的半截状态。 * 目标 origin 从 iframe 自己的 URL 推导(触屏走的 viewToken 链接可能是别的 * host/port),推导不出来就放弃这次下发——绝不用 `*` 广播。 */ export declare function postWorkbenchTermAppearance(frame: { contentWindow?: { postMessage(data: unknown, targetOrigin: string): void; } | null; } | null | undefined, frameUrl: string | null | undefined, termStyle: WorkbenchTermStyle, skin: WorkbenchSkinId, baseUrl?: string): boolean; /** 只用到 dataset 的最小结构,方便单测注入一个假根节点。 */ export interface WorkbenchAppearanceRoot { readonly dataset: Record; } export interface WorkbenchRootAttributes { /** 生效的配色,写进 `data-skin`。 */ skin: WorkbenchSkinId; /** 生效的明暗族,写进 `data-theme`,让既有的明暗组件规则跟着走。 */ theme: ResolvedTheme; } /** 进工作台前文档根上原本挂着什么(全站 skin / 明暗),离开时原样放回去。 */ export interface WorkbenchRootSnapshot { skin?: string; theme?: string; } export declare function applyWorkbenchRootAttributes(root: WorkbenchAppearanceRoot | null | undefined, attributes: WorkbenchRootAttributes): WorkbenchRootSnapshot; export declare function restoreWorkbenchRootAttributes(root: WorkbenchAppearanceRoot | null | undefined, snapshot: WorkbenchRootSnapshot): void; /** * 外观只有一个状态源:桌面 `⋯ → 外观`、移动列表页 `◐`、终端标题栏的 * 「阅读|经典」分段控件全部读写这一个 store,任一处改动另外两处立刻跟着变。 * 跨 tab 同步靠 `storage` 事件;跟随系统靠 `matchMedia` 的 change。 */ export interface WorkbenchAppearanceSnapshot { appearance: WorkbenchAppearance; /** 实际生效的配色(已解析 mode + 系统偏好)。 */ skin: WorkbenchSkinId; theme: ResolvedTheme; } export interface WorkbenchAppearanceEnvironment { storage: WorkbenchStorage | null; root: WorkbenchAppearanceRoot | null; /** 首次进工作台、本地还没有记录时 mode 的初值(全站 ThemeMode)。 */ inheritedMode: ThemeMode; prefersDark(): boolean; /** 订阅系统明暗变化;返回退订函数。 */ onSystemThemeChange?(listener: () => void): () => void; /** 订阅跨 tab 的 localStorage 变化;返回退订函数。 */ onStorageChange?(listener: (key: string | null, value: string | null) => void): () => void; } /** 浏览器环境。没有 window(组件测试跑在 node 上)时返回 null,一切走默认值。 */ export declare function browserAppearanceEnvironment(): WorkbenchAppearanceEnvironment | null; export declare class WorkbenchAppearanceStore { private env; private envResolved; private value; private snapshot; private readonly listeners; private mounted; private disposers; private rootSnapshot; /** 环境只认一次:测试注入假实现,浏览器里第一次取值时自己接上真的。 */ configure(env: WorkbenchAppearanceEnvironment | null): void; getSnapshot(): WorkbenchAppearanceSnapshot; subscribe(listener: () => void): () => void; set(next: WorkbenchAppearance): void; /** * 把外观挂到文档根上,并接好系统明暗 / 跨 tab 两个监听。返回卸载函数: * 离开工作台时把 `data-skin` / `data-theme` 原样还给全站机制(工作台的 4 套 * 配色是局部调色板,不该在别的页面继续生效)。 */ mount(): () => void; /** 测试用:把 store 恢复到「还没接过环境」的状态。 */ reset(): void; private ensureEnv; private bind; private unbind; /** * 重新解析并落地。全站那套明暗仲裁(ui.ts)也在写 `data-theme`,工作台在场时 * 由这里把自己的解析结果盖回去 —— 所以它必须幂等:没变化就不换快照对象、也不 * 通知订阅者,免得每来一次全站事件就把工作台整页重绘。 */ reapply(): void; private refresh; private buildSnapshot; } export declare const workbenchAppearance: WorkbenchAppearanceStore; //# sourceMappingURL=agent-workbench-appearance.d.ts.map